---
title: "telephony.complete / web.complete"
description: "Icke-blockerande webhook som skickas när ett samtal avslutas, med transkription, inspelnings-URL och mätvärden."
---

En slutförandehändelse utlöses efter att varje samtal avslutas — inkommande telefoni,
utgående telefoni, webbsamtal eller testsamtal (byggarens mikrofonsession). Den är
**icke-blockerande**: svara med valfri 2xx.

Händelsen levereras via båda vägarna:

* **[Webhook-slutpunkter](/sv/webhooks/endpoints)** tar emot
  `telephony.complete` (telefonsamtal) eller `web.complete` (webbsamtal och
  byggarens mikrofontestsamtal) med den stabila nyttolasten som dokumenteras nedan,
  ett `event_id` per leverans, en timeout på 30 s och
  [återförsök i upp till 24 h](/sv/webhooks/overview).
* **Den [äldre webhooken med en enda URL](/api-reference/organizations#legacy-single-url-webhook)**
  tar emot ett synkront försök (timeout på 10 s, inga återförsök) med en
  något annorlunda nyttolast — se
  [Skillnader i äldre nyttolast](#legacy-payload-differences).

## Begärandenyttolast (endpointleveranser)

```json
{
  "data": {
    "billable_minutes": 1.25,
    "billing_total_cents": 8,
    "call_id": 987654321,
    "direction": "inbound",
    "duration_seconds": 54,
    "end_reason": "user_hangup",
    "end_time": "2026-04-20T18:25:04.822Z",
    "extracted_data": {
      "status": "completed",
      "fields": {
        "customer_name": "Alex Morgan",
        "appointment_date": "2026-04-23"
      },
      "evidence": {
        "customer_name": {
          "quote": "My name is Alex Morgan",
          "speaker_role": "caller",
          "turn_index": 4
        },
        "appointment_date": {
          "quote": "April 23 works for me",
          "speaker_role": "caller",
          "turn_index": 7
        }
      },
      "verification": "verified",
      "field_reasons": {},
      "schema_version": "92850758e231a3c95a..."
    },
    "from_number": "+14155550199",
    "product": "spark",
    "recording_stopped_at": null,
    "recording_stopped_reason": null,
    "recording_url": "https://storage.example.com/…",
    "start_time": "2026-04-20T18:24:10.113Z",
    "status": "completed",
    "summary": "Alex Morgan called to book a follow-up visit and confirmed Thursday, April 23. The agent read the appointment back and Alex asked for a reminder text the day before.",
    "to_number": "+15551234567",
    "transcripts": [ /* see Transcript format */ ],
    "transfer_number": null,
    "unresolved_variables": ["campaign_owner"],
    "variables": {"campaign_name": "Spring renewals"},
    "voice": "john"
  },
  "event_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
  "type": "telephony.complete"
}
```

| Fält | Typ | Beskrivning |
|-------|------|-------------|
| `call_id` | heltal | Stabilt för varje händelse för detta samtal |
| `agent_id` | heltal \| null | Agent som hanterade samtalet, när en tilldelades |
| `agent_name` | sträng \| null | Agent som hanterade samtalet, när en tilldelades |
| `direction` | sträng | `inbound`, `outbound`, `web`, `test`. Historiska nyttolaster kan innehålla äldre värden för `mic` eller `widget` |
| `from_number`, `to_number` | sträng | E.164. `from_number` är den bokstavliga strängen `"web"` för webbsamtal och testsamtal |
| `origin_domain` | sträng | **Endast webb/test** — sidans ursprung som var värd för widgeten (tomt för mikrofonsessioner) |
| `start_time`, `end_time` | tidsstämpel | ISO 8601 UTC |
| `duration_seconds` | heltal \| null | Härleds från start/slut |
| `status` | sträng | `completed` eller `failed` |
| `end_reason` | sträng | Se tabellen nedan |
| `product`, `voice` | sträng | Agentkonfiguration som gällde vid samtalstillfället |
| `variables` | objekt | Indatavariabler som sparades som ögonblicksbild när samtalet startade |
| `unresolved_variables` | matris | Variabelnamn som refereras av samtalskonfigurationen men inte angavs när samtalet startade |
| `transfer_number` | sträng \| null | Anges när samtalet överfördes |
| `recording_url` | sträng \| null | Tidsbegränsad signerad URL; ladda ned den omgående. `null` när ingen inspelningsartefakt är tillgänglig |
| `recording_stopped_at` | tidsstämpel \| null | När inspelningen stoppades innan samtalet avslutades; inget efter denna tidpunkt finns med i inspelningen. `null` när inspelningen fortsatte till slutet av samtalet eller samtalet inte spelades in |
| `recording_stopped_reason` | sträng \| null | Varför inspelningen stoppades i förtid. `warm_transfer`: den stoppas precis innan en [varm överföring](/sv/guides/call-transfers) ringer destinationen och förblir avstängd även om överföringen misslyckas och uppringaren återvänder till agenten. Behandla andra värden som informativa; fler kan läggas till. `null` när `recording_stopped_at` är `null` |
| `billable_minutes` | tal | Debiterade minuter, avrundade till närmaste kvarts minut (intervall på 15 sekunder, minst 0.25). Samtal som går direkt till röstbrevlådan rapporterar fortfarande sina faktiska uppmätta minuter här, men avgiften är begränsad till en minut enligt planens pris. |
| `billing_total_cents` | heltal | Amerikanska cent |
| `transcripts` | matris | Transkriptposter per tur; kan vara tom när ett transkript inte är tillgängligt |
| `summary` | sträng \| null | Sammanfattning efter samtalet på klarspråk (två till fyra meningar löpande text, högst 2 000 tecken, skriven på samtalets språk: agentens primära språk om inte samtalet åsidosatte det) när agenten har **Generera en samtalssammanfattning** aktiverat under **Data att samla in** i agentbyggaren (`call_summary_enabled` på [Agent](/api-reference/agents)) och genereringen lyckades; annars `null`, inklusive för samtal som är för korta för att sammanfatta, samtal utan transkript och agenter med inspelning avstängd. Sammanfattningen genereras i samma steg efter samtalet som extraheringen, så inställningen **Vänta på extraherade data innan webhooken för avslutat samtal skickas** (`extraction_blocks_completion`) avgör om denna händelse väntar på den: i ett vänteläge är sammanfattningen klar innan denna händelse skickas (sammanfattningen får högst 30 sekunder inom stegets budget på 75 sekunder); när händelsen inte väntar (`never` eller en överföring under `except_transfers`) är detta fält `null` och sammanfattningen skickas **inte** på nytt i `call.data_extracted`, så läs den senare från `summary` i [Call-objektet](/api-reference/calls#call-object). Finns även på samtalssidan i dashboarden |
| `extracted_data` | objekt \| null | Strukturerat extraheringsresultat med `status`, `fields`, `evidence`, `verification`, `field_reasons` och `schema_version`. Varje fält som inte är null har det exakta strukturellt kontrollerade citatet (högst 1 000 tecken), samt talarroll och turindex; längre citat som returneras av modellen avvisas i stället för att trunkeras. Evidens är `null` när fältet är `null`. `verified` innebär att varje kandidat fick exakt ett giltigt oberoende utlåtande. `unavailable` omfattar även felaktiga eller partiella verifierarutdata; giltiga partiella utlåtanden tillämpas fortfarande, medan kandidater utan ett giltigt utlåtande sätts till null. `status` är `completed`, `failed`, `exhausted`, `skipped` eller `skipped_recording_disabled`; `null` när agenten inte hade några extraheringsfält |
| `steps_trace` | matris | Endast för samtal vars agent har steg eller överlämningar. Ordningen som samtalet rörde sig genom dem: varje post har `t_ms` (millisekunder sedan samtalet startade), `agent` (agentens nyckel, `a` plus dess ID), `step` (steg-ID:t, eller `null` vid en överlämning till en agent utan steg) och `via` (`start`, `model`, `condition`, `on_enter` eller `handoff`). Innehåller endast ID:n och tider, så den behålls även när inspelning är avstängd |
| `collected_values` | objekt | Endast för samtal vars agent har steg eller överlämningar. Värdena som agenten samlade in i sina steg, nycklade efter insamlingsfältnamn, plus `<tool>_result` för varje stegs `on_enter_tool`-resultat. Utelämnas när samtalets innehåll inte behålls (inspelning avstängd) |

### Avslutsorsaker

| Värde | Betydelse |
|-------|---------|
| `user_hangup` | Motparten lade på först |
| `ai_hangup` | AI:n avslutade samtalet avsiktligt |
| `ai_transfer` | AI:n kopplade samtalet vidare; `transfer_number` är angivet |
| `ai_warm_transfer` | AI:n slutförde en varm (övervakad) vidarekoppling |
| `voicemail_hangup` | Röstbrevlåda upptäcktes och samtalet avslutades enligt din `voicemail_action` |
| `max_duration` | Samtalet nådde gränsen för maximal längd: agentens eller samtalets egen gräns, eller plattformens gräns på 6 timmar. Ett samtal som AI:n eller uppringaren avslutar efter varningen om tidsgränsen behåller `ai_hangup` eller `user_hangup` |
| `superseded` | Sessionen ersattes av en nyare |
| `inbound_config_webhook_failed` | Inkommande samtal till ett nummer i webhook-läge: [konfigurationswebhooken](/sv/webhooks/call-incoming) returnerade inte en användbar konfiguration, så samtalet avslutades innan en agent svarade. `status` är `failed` och samtalet debiteras inte |
| `unknown` | Orsaken till avslutet kunde inte fastställas |

## Transkriptformat

Varje post i `transcripts` är en samtalstur. Roller är
`user` (uppringarens tal), `model` (agentens tal **och** verktygsanrop),
`tool` (verktygsresultat) och `system` (samtalshändelser som språkbyten).

```json
[
  {
    "role": "user",
    "content_type": "text/plain",
    "content": "Hi, I'm calling about my appointment.",
    "start_ms": 1200,
    "end_ms":   4100,
    "audio_url": "https://storage.example.com/…"
  },
  {
    "role": "model",
    "content_type": "text/plain",
    "content": "Sure, what date works best?",
    "start_ms": 4200,
    "end_ms":   6100
  },
  {
    "role": "model",
    "content_type": "application/json",
    "content": {
      "tool_call": "search_appointments",
      "arguments": { "date": "2026-04-21" }
    }
  },
  {
    "role": "tool",
    "content_type": "application/json",
    "content": {
      "tool_name": "search_appointments",
      "response": { "available_slots": ["9:00 AM", "2:00 PM"] }
    }
  }
]
```

| Fält | Typ | Beskrivning |
|-------|------|-------------|
| `role` | sträng | `user`, `model`, `tool` eller `system` |
| `content_type` | sträng | `text/plain` för tal; `application/json` för verktygsanrop, verktygsresultat och systemhändelser |
| `content` | sträng \| objekt | Taltext eller det strukturerade objektet som visas ovan. Verktygsanrop: `{"tool_call": name, "arguments": {…}}`. Verktygsresultat: `{"tool_name": name, "response": {…}}` |
| `start_ms`, `end_ms` | heltal | Förskjutningar från samtalsstart, i ms. Finns när ljudtidtagning är känd |
| `ttfa_ms` | heltal | Tid till första ljudet för en `model`-tur, när den mäts |
| `audio_url`, `audio_urls` | sträng / matris | Tidsbegränsade signerade URL:er för turens ljud, när ljud spelas in per tur |

Använd
[`GET /v1/calls/{call_id}/history`](/api-reference/calls#get-history)
för den fullständigt strukturerade turhistoriken (med avbrottsmarkörer,
bekräftelseuppmaningar och råa positioner).

## Skillnader i äldre payload

Det äldre webhook-omslaget med en enda URL är
`{"type": "telephony.complete" | "web.complete", "data": {…}}` utan
**något `event_id`**, och dess `data` skiljer sig från endpoint-payloaden:

Den äldre slutförandepayloaden innehåller även `agent_id` och `agent_name`.

* Turmatrisen finns under **`history`**, inte `transcripts` (samma
  turschema som ovan).
* Fältuppsättningen är den råa rapporten vid samtalets slut och kan innehålla
  ytterligare interna fält utöver tabellen ovan — behandla okända
  fält som information.
* Webbsamtal (`direction: "web"`) **utelämnar** `from_number` / `to_number`
  och lägger till `origin_domain`.
* Mikrofonsamtal för test i Builder rapporteras som `telephony.complete` på den
  äldre sökvägen (endpoint-systemet mappar dem till `web.complete`).
* **Överföringssamordning:** när ett samtal avslutas med en överföring anropas
  den äldre webhooken synkront och kan svara
  `{"transfer_ready": false}` för att signalera att överföringens mål inte är
  redo. Alla andra svar (eller ingen äldre webhook) låter överföringen
  fortsätta. Endpoint-leveranser konsulteras aldrig för detta.

---

## Exempelhanterare

<CodeGroup>
```python Python (FastAPI)
import hashlib
import hmac
import json
import os

from fastapi import FastAPI, HTTPException, Request

app = FastAPI()
SECRET = os.environ["THUNDERPHONE_WEBHOOK_SECRET"]

def verify(body: bytes, signature: str) -> bool:
    expected = hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature or "")

@app.post("/thunderphone-webhook")
async def webhook(request: Request):
    body = await request.body()
    if not verify(body, request.headers.get("X-ThunderPhone-Signature", "")):
        raise HTTPException(status_code=401)

    event = json.loads(body)
    if event["type"] in ("telephony.complete", "web.complete"):
        data = event["data"]
        # Endpoint deliveries use "transcripts"; the legacy webhook uses "history".
        turns = data.get("transcripts") or data.get("history") or []
        await persist_call_record(
            call_id=data["call_id"],
            turns=turns,
            recording_url=data.get("recording_url"),
        )
        if data["end_reason"] in ("ai_transfer", "ai_warm_transfer"):
            await notify_team(data.get("transfer_number"), data["call_id"])
    return {"ok": True}
```

```javascript Node.js (Express)
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.THUNDERPHONE_WEBHOOK_SECRET;

function verify(body, signature) {
  const expected = crypto.createHmac("sha256", SECRET).update(body).digest("hex");
  return signature &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

app.post(
  "/thunderphone-webhook",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    if (!verify(req.body, req.header("X-ThunderPhone-Signature"))) {
      return res.sendStatus(401);
    }
    const event = JSON.parse(req.body.toString("utf8"));
    if (["telephony.complete", "web.complete"].includes(event.type)) {
      const data = event.data;
      // Endpoint deliveries use "transcripts"; the legacy webhook uses "history".
      const turns = data.transcripts ?? data.history ?? [];
      await persistCallRecord({ ...data, turns });
      if (["ai_transfer", "ai_warm_transfer"].includes(data.end_reason)) {
        await notifyTeam(data.transfer_number, data.call_id);
      }
    }
    res.json({ ok: true });
  },
);
```
</CodeGroup>

---

## Vanliga användningsfall

<CardGroup cols={2}>
  <Card title="CRM-integration" icon="database">
    Spara varje samtals transkript och inspelnings-URL tillsammans med dina
    kundregister.
  </Card>
  <Card title="Analys" icon="chart-line">
    Strömma transkript till en pipeline för ämnesmodellering, extrahering av
    CSAT-signaler eller övervakning av överföringsfrekvens.
  </Card>
  <Card title="Kvalitetsgranskning" icon="clipboard-check">
    Öppna samtal i ett QA-verktyg för manuell granskning, eller kör dem genom
    din egen utvärderingsmodell.
  </Card>
  <Card title="Aviseringar" icon="bell">
    Avisera en mänsklig teammedlem vid överföring eller fel.
  </Card>
</CardGroup>

---

## Relaterat

<CardGroup cols={2}>
  <Card title="telephony.incoming / web.incoming" icon="phone" href="/sv/webhooks/call-incoming">
    Den blockerande motsvarigheten som körs när samtalet startar.
  </Card>
  <Card title="Händelsekatalog" icon="list" href="/sv/webhooks/events">
    Andra händelsetyper som du kan prenumerera på.
  </Card>
  <Card title="API för samtalshistorik" icon="phone" href="/api-reference/calls">
    Samma data är tillgängliga via REST för återfyllning eller uppspelning.
  </Card>
</CardGroup>
