---
title: "telephony.complete / web.complete"
description: "Ikke-blokkerende webhook levert når en samtale avsluttes, med transkripsjon, URL til opptak og målinger."
---

En fullføringshendelse utløses etter at hver samtale avsluttes — innkommende telefoni,
utgående telefoni, nettsamtale eller testsamtale (mikrofonøkt i byggeren). Den er
**ikke-blokkerende**: svar med en hvilken som helst 2xx-status.

Hendelsen leveres via begge banene:

* **[Webhook-endepunkter](/nb/webhooks/endpoints)** mottar
  `telephony.complete` (telefonsamtaler) eller `web.complete` (nettsamtaler og
  mikrofontestsamtaler i byggeren) med den stabile nyttelasten dokumentert nedenfor,
  en `event_id` per levering, en tidsgrense på 30 s og
  [nye forsøk i opptil 24 t](/nb/webhooks/overview).
* **Den [eldre webhooken med én URL](/api-reference/organizations#legacy-single-url-webhook)**
  mottar ett synkront forsøk (tidsgrense på 10 s, ingen nye forsøk) med en
  litt annerledes nyttelast — se
  [Forskjeller i eldre nyttelast](#legacy-payload-differences).

## Forespørselsnyttelast (endepunktleveringer)

```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"
}
```

| Felt | Type | Beskrivelse |
|-------|------|-------------|
| `call_id` | heltall | Stabil på tvers av alle hendelser for denne samtalen |
| `agent_id` | heltall \| null | Stemmeagenten som håndterte samtalen, når en var tilordnet |
| `agent_name` | streng \| null | Stemmeagenten som håndterte samtalen, når en var tilordnet |
| `direction` | streng | `inbound`, `outbound`, `web`, `test`. Historiske nyttelaster kan inneholde eldre `mic`- eller `widget`-verdier |
| `from_number`, `to_number` | streng | E.164. `from_number` er den bokstavelige verdien `"web"` for nettsamtaler og testsamtaler |
| `origin_domain` | streng | **Kun web/test** — sideopprinnelsen som var vert for widgeten (tom for mic-økter) |
| `start_time`, `end_time` | tidsstempel | ISO 8601 UTC |
| `duration_seconds` | heltall \| null | Avledet fra start/slutt |
| `status` | streng | `completed` eller `failed` |
| `end_reason` | streng | Se tabellen nedenfor |
| `product`, `voice` | streng | Stemmeagentkonfigurasjonen som var aktiv på samtaletidspunktet |
| `variables` | objekt | Inndatavariabler med øyeblikksbilde fra da samtalen startet |
| `unresolved_variables` | matrise | Variabelnavn som refereres av samtalekonfigurasjonen, men som ikke ble oppgitt ved samtalestart |
| `transfer_number` | streng \| null | Angis når samtalen ble overført |
| `recording_url` | streng \| null | Utløpende signert URL; last ned raskt. `null` når ingen opptaksartefakt er tilgjengelig |
| `recording_stopped_at` | tidsstempel \| null | Når opptaket ble stoppet før samtalen var over; ingenting etter dette tidspunktet er med i opptaket. `null` når opptaket fortsatte til slutten av samtalen, eller samtalen ikke ble tatt opp |
| `recording_stopped_reason` | streng \| null | Hvorfor opptaket stoppet tidlig. `warm_transfer`: det stopper rett før en [varm overføring](/nb/guides/call-transfers) ringer destinasjonen, og forblir avslått selv om overføringen mislykkes og innringeren kommer tilbake til stemmeagenten. Behandle andre verdier som informasjon; flere kan bli lagt til. `null` når `recording_stopped_at` er `null` |
| `billable_minutes` | tall | Fakturerte minutter, avrundet til nærmeste kvart minutt (intervaller på 15 sekunder, minimum 0.25). Samtaler som går rett til talepost rapporterer fortsatt sine faktiske målte minutter her, men kostnaden er begrenset til ett minutt til planprisen. |
| `billing_total_cents` | heltall | USD-cent |
| `transcripts` | matrise | Transkripsjonsoppføringer per tur; kan være tom når en transkripsjon ikke er tilgjengelig |
| `summary` | streng \| null | Sammendrag etter samtalen i klart språk (to til fire setninger med løpende tekst, maksimalt 2 000 tegn, skrevet på samtalens språk: stemmeagentens primærspråk med mindre samtalen overstyrte det) når stemmeagenten har **Generer et samtalesammendrag** aktivert under **Data som skal samles inn** i stemmeagentbyggeren (`call_summary_enabled` på [Agent](/api-reference/agents)) og genereringen lykkes; ellers `null`, inkludert for samtaler som er for korte til å oppsummere, samtaler uten transkripsjon og stemmeagenter med opptak deaktivert. Sammendraget genereres i samme steg etter samtalen som uthenting, så innstillingen **Vent på uthentede data før webhooken for fullført samtale sendes** (`extraction_blocks_completion`) avgjør om denne hendelsen venter på det: I en ventende modus er sammendraget klart før denne hendelsen sendes (selve sammendraget får maksimalt 30 sekunder innenfor stegets budsjett på 75 sekunder); når hendelsen ikke venter (`never`, eller en overføring under `except_transfers`), er dette feltet `null`, og sammendraget sendes **ikke** på nytt i `call.data_extracted`, så les det senere fra `summary` på [Call-objektet](/api-reference/calls#call-object). Også tilgjengelig på samtalesiden i dashbordet |
| `extracted_data` | objekt \| null | Strukturert uthentingsresultat med `status`, `fields`, `evidence`, `verification`, `field_reasons` og `schema_version`. Hvert felt som ikke er null, har det nøyaktige strukturelt kontrollerte sitatet (maksimalt 1 000 tegn), samt talerrollen og turindeksen; lengre sitater returnert av modellen avvises i stedet for å avkortes. Bevis er `null` når feltet er `null`. `verified` betyr at hver kandidat mottok nøyaktig én gyldig, uavhengig vurdering. `unavailable` dekker også feilformatert eller delvis verifikatorutdata; gyldige delvise vurderinger gjelder fortsatt, mens kandidater uten én gyldig vurdering settes til null. `status` er `completed`, `failed`, `exhausted`, `skipped` eller `skipped_recording_disabled`; `null` når stemmeagenten ikke hadde noen uthentingsfelt |
| `steps_trace` | matrise | Bare for samtaler der stemmeagenten har steg eller overleveringer. Rekkefølgen samtalen beveget seg gjennom dem i: Hver oppføring har `t_ms` (millisekunder siden samtalen startet), `agent` (stemmeagentens nøkkel, `a` pluss ID-en), `step` (steg-ID-en, eller `null` ved en overlevering til en stemmeagent uten steg) og `via` (`start`, `model`, `condition`, `on_enter` eller `handoff`). Inneholder bare ID-er og tidspunkter, så den beholdes selv når opptak er deaktivert |
| `collected_values` | objekt | Bare for samtaler der stemmeagenten har steg eller overleveringer. Verdiene stemmeagenten samlet inn i stegene sine, nøkkelbasert på navnet på innsamlingsfeltet, samt `<tool>_result` for hvert stegs `on_enter_tool`-resultat. Utelates når samtalens innhold ikke beholdes (opptak deaktivert) |

### Sluttårsaker

| Verdi | Betydning |
|-------|---------|
| `user_hangup` | Den eksterne parten la på først |
| `ai_hangup` | AI-en avsluttet samtalen med hensikt |
| `ai_transfer` | AI-en videresendte samtalen; `transfer_number` er angitt |
| `ai_warm_transfer` | AI-en fullførte en varm (betjent) viderekobling |
| `voicemail_hangup` | Telefonsvarer ble oppdaget, og samtalen ble avsluttet i henhold til `voicemail_action` |
| `max_duration` | Samtalen nådde maksimal varighetsgrense: agentens eller samtalens egen grense, eller plattformgrensen på 6 timer. En samtale som AI-en eller innringeren avslutter etter advarselen om tidsgrensen, beholder `ai_hangup` eller `user_hangup` |
| `superseded` | Økten ble erstattet av en nyere økt |
| `inbound_config_webhook_failed` | Innkommende samtale til et nummer i webhook-modus: [konfigurasjonswebhooken](/nb/webhooks/call-incoming) returnerte ikke en brukbar konfigurasjon, så samtalen ble avsluttet før en agent svarte. `status` er `failed`, og samtalen faktureres ikke |
| `unknown` | Avslutningsårsaken kunne ikke fastslås |

## Transkriptformat

Hver oppføring i `transcripts` er én samtaleomgang. Rollene er
`user` (innringerens tale), `model` (agentens tale **og** verktøykall),
`tool` (verktøyresultater) og `system` (anropshendelser, som
språkbytter).

```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"] }
    }
  }
]
```

| Felt | Type | Beskrivelse |
|-------|------|-------------|
| `role` | streng | `user`, `model`, `tool` eller `system` |
| `content_type` | streng | `text/plain` for tale; `application/json` for verktøykall, verktøyresultater og systemhendelser |
| `content` | streng \| objekt | Taletekst eller det strukturerte objektet vist ovenfor. Verktøykall: `{"tool_call": name, "arguments": {…}}`. Verktøyresultater: `{"tool_name": name, "response": {…}}` |
| `start_ms`, `end_ms` | heltall | Forskyvninger fra anropsstart, i ms. Finnes når lydtiming er kjent |
| `ttfa_ms` | heltall | Tid til første lyd for en `model`-omgang, når målt |
| `audio_url`, `audio_urls` | streng / matrise | Utløpende signerte URL-er for omgangens lyd, når lyd tas opp per omgang |

For den fullstendig strukturerte omgangshistorikken (med avbruddsmarkører,
bekreftelsesforespørsler og rå posisjoner), bruk
[`GET /v1/calls/{call_id}/history`](/api-reference/calls#get-history).

## Forskjeller i eldre nyttelast

Den eldre webhook-innpakningen med én URL er
`{"type": "telephony.complete" | "web.complete", "data": {…}}` uten
**`event_id`**, og `data`-feltet skiller seg fra endepunktets nyttelast:

Den eldre fullføringsnyttelasten inneholder også `agent_id` og `agent_name`.

* Omgangslisten ligger under **`history`**, ikke `transcripts` (samme
  omgangsskjema som ovenfor).
* Feltsettet er den rå rapporten ved anropsslutt og kan inneholde
  ekstra interne felt utover tabellen ovenfor — behandle ukjente
  felt som informasjon.
* Nettanrop (`direction: "web"`) **utelater** `from_number` / `to_number`
  og legger til `origin_domain`.
* Byggerens mikrofontestanrop rapporteres som `telephony.complete` på den eldre
  banen (endepunktssystemet tilordner dem til `web.complete`).
* **Koordinering av overføring:** Når et anrop avsluttes med en overføring,
  kalles den eldre webhooken synkront og kan svare
  `{"transfer_ready": false}` for å signalisere at målet for overføringen ikke
  er klart. Ethvert annet svar (eller ingen eldre webhook) lar overføringen
  fortsette. Leveringer til endepunktet blir aldri konsultert for dette.

---

## Eksempelbehandler

<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>

---

## Vanlige bruksområder

<CardGroup cols={2}>
  <Card title="CRM-integrasjon" icon="database">
    Lagre hvert samtaletranskript og hver opptaks-URL sammen med
    kundepostene dine.
  </Card>
  <Card title="Analyse" icon="chart-line">
    Send transkripter til en pipeline for emnemodellering, uttrekking av
    CSAT-signaler eller overvåking av overføringsrate.
  </Card>
  <Card title="Kvalitetsgjennomgang" icon="clipboard-check">
    Åpne samtaler i et QA-verktøy for menneskelig gjennomgang, eller kjør dem
    gjennom din egen evalueringsmodell.
  </Card>
  <Card title="Varsler" icon="bell">
    Varsle en menneskelig kollega ved overføring eller feil.
  </Card>
</CardGroup>

---

## Relatert

<CardGroup cols={2}>
  <Card title="telephony.incoming / web.incoming" icon="phone" href="/nb/webhooks/call-incoming">
    Den blokkerende motparten som kjører når samtalen starter.
  </Card>
  <Card title="Hendelseskatalog" icon="list" href="/nb/webhooks/events">
    Andre hendelsestyper du kan abonnere på.
  </Card>
  <Card title="API for samtalehistorikk" icon="phone" href="/api-reference/calls">
    De samme dataene er tilgjengelige via REST for etterfylling eller avspilling på nytt.
  </Card>
</CardGroup>
