---
title: "telephony.complete / web.complete"
description: "Niet-blokkerende webhook die wordt verzonden wanneer een oproep eindigt, met transcriptie, opname-URL en statistieken."
---

Een voltooiingsevent wordt geactiveerd nadat elk gesprek is beëindigd — inkomende telefonie,
uitgaande telefonie, webgesprek of testgesprek (microfoonsessie in de builder). Het is
**niet-blokkerend**: reageer met een willekeurige 2xx.

Het event wordt via beide paden geleverd:

* **[Webhook-eindpunten](/nl/webhooks/endpoints)** ontvangen
  `telephony.complete` (telefoongesprekken) of `web.complete` (webgesprekken en
  microfoontestgesprekken in de builder) met de stabiele payload die hieronder wordt gedocumenteerd,
  een `event_id` per levering, een time-out van 30 s en
  [opnieuw proberen gedurende maximaal 24 h](/nl/webhooks/overview).
* **De [verouderde webhook met één URL](/api-reference/organizations#legacy-single-url-webhook)**
  ontvangt één synchrone poging (time-out van 10 s, geen nieuwe pogingen) met een
  iets andere payload — zie
  [Verschillen in verouderde payloads](#legacy-payload-differences).

## Aanvraagpayload (endpointleveringen)

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

| Veld | Type | Beschrijving |
|-------|------|-------------|
| `call_id` | integer | Stabiel in elke gebeurtenis voor deze oproep |
| `agent_id` | integer \| null | Agent die de oproep heeft afgehandeld, wanneer er een was toegewezen |
| `agent_name` | string \| null | Agent die de oproep heeft afgehandeld, wanneer er een was toegewezen |
| `direction` | string | `inbound`, `outbound`, `web`, `test`. Historische payloads kunnen verouderde waarden `mic` of `widget` bevatten |
| `from_number`, `to_number` | string | E.164. `from_number` is de letterlijke waarde `"web"` voor weboproepen en testoproepen |
| `origin_domain` | string | **Alleen web/test** — de pagina-origin waarop de widget werd gehost (leeg voor mic-sessies) |
| `start_time`, `end_time` | timestamp | ISO 8601 UTC |
| `duration_seconds` | integer \| null | Afgeleid van start/einde |
| `status` | string | `completed` of `failed` |
| `end_reason` | string | Zie de onderstaande tabel |
| `product`, `voice` | string | Agentconfiguratie die van kracht was op het moment van de oproep |
| `variables` | object | Invoervariabelen waarvan een momentopname is gemaakt toen de oproep begon |
| `unresolved_variables` | array | Variabelenamen waarnaar de oproepconfiguratie verwijst maar die niet bij het begin van de oproep zijn opgegeven |
| `transfer_number` | string \| null | Ingesteld wanneer de oproep is doorgeschakeld |
| `recording_url` | string \| null | Verlopende ondertekende URL; download deze snel. `null` wanneer geen opname-artefact beschikbaar is |
| `recording_stopped_at` | timestamp \| null | Wanneer de opname werd gestopt voordat de oproep eindigde; niets na dit moment staat in de opname. `null` wanneer de opname tot het einde van de oproep liep of de oproep niet werd opgenomen |
| `recording_stopped_reason` | string \| null | Waarom de opname vroegtijdig stopte. `warm_transfer`: deze stopt vlak voordat een [warme doorverbinding](/nl/guides/call-transfers) de bestemming belt, en blijft uitgeschakeld, zelfs als de doorverbinding mislukt en de beller terugkeert naar de agent. Behandel andere waarden als informatief; er kunnen meer waarden worden toegevoegd. `null` wanneer `recording_stopped_at` `null` is |
| `billable_minutes` | number | Gefactureerde minuten, afgerond op het dichtstbijzijnde kwartier minuut (stappen van 15 seconden, minimaal 0.25). Oproepen die rechtstreeks naar voicemail gaan rapporteren hier nog steeds hun werkelijke gemeten minuten, maar de kosten zijn gemaximeerd op één minuut tegen het paktettarief. |
| `billing_total_cents` | integer | USD-centen |
| `transcripts` | array | Transcriptitems per beurt; kan leeg zijn wanneer een transcript niet beschikbaar is |
| `summary` | string \| null | Samenvatting na de oproep in gewone taal (twee tot vier zinnen proza, maximaal 2.000 tekens, geschreven in de taal van de oproep: de primaire taal van de agent, tenzij de oproep deze overschreef) wanneer de agent **Een oproepsamenvatting genereren** heeft ingeschakeld onder **Te verzamelen gegevens** in de agentbouwer (`call_summary_enabled` op de [Agent](/api-reference/agents)) en het genereren is geslaagd; anders `null`, ook voor oproepen die te kort zijn om samen te vatten, oproepen zonder transcript en agents waarvoor opname is uitgeschakeld. De samenvatting wordt gegenereerd in dezelfde stap na de oproep als extractie, dus de instelling **Wachten op geëxtraheerde gegevens voordat de webhook voor een voltooide oproep wordt verzonden** (`extraction_blocks_completion`) bepaalt of deze gebeurtenis erop wacht: in een wachtmodus is de samenvatting gereed voordat deze gebeurtenis wordt verzonden (de samenvatting zelf krijgt maximaal 30 seconden binnen het budget van 75 seconden van de stap); wanneer de gebeurtenis niet wacht (`never`, of een doorverbinding onder `except_transfers`) is dit veld `null` en wordt de samenvatting **niet** opnieuw verzonden in `call.data_extracted`, dus lees deze later uit `summary` op het [Oproepobject](/api-reference/calls#call-object). Ook beschikbaar op de oproeppagina in het dashboard |
| `extracted_data` | object \| null | Gestructureerd extractieresultaat met `status`, `fields`, `evidence`, `verification`, `field_reasons` en `schema_version`. Elk niet-nullveld bevat het exacte structureel gecontroleerde citaat (maximaal 1.000 tekens), plus de sprekersrol en beurtindex; langere door het model geretourneerde citaten worden afgewezen in plaats van afgekapt. Bewijs is `null` wanneer het veld `null` is. `verified` betekent dat elke kandidaat precies één geldig onafhankelijk oordeel heeft ontvangen. `unavailable` omvat ook onjuist gevormde of gedeeltelijke uitvoer van de verificateur; geldige gedeeltelijke oordelen blijven van toepassing, terwijl kandidaten zonder één geldig oordeel op null worden gezet. `status` is `completed`, `failed`, `exhausted`, `skipped` of `skipped_recording_disabled`; `null` wanneer de agent geen extractievelden had |
| `steps_trace` | array | Alleen voor oproepen waarvan de agent stappen of overdrachten heeft. De volgorde waarin de oproep deze doorliep: elk item bevat `t_ms` (milliseconden sinds de oproep begon), `agent` (de sleutel van de agent, `a` plus de ID), `step` (de stap-ID, of `null` bij een overdracht naar een agent zonder stappen) en `via` (`start`, `model`, `condition`, `on_enter` of `handoff`). Bevat alleen ID's en tijden, dus blijft bewaard, ook wanneer opname is uitgeschakeld |
| `collected_values` | object | Alleen voor oproepen waarvan de agent stappen of overdrachten heeft. De waarden die de agent in zijn stappen heeft verzameld, ingedeeld op naam van het verzamelveld, plus `<tool>_result` voor het `on_enter_tool`-resultaat van elke stap. Weggelaten wanneer de inhoud van de oproep niet wordt bewaard (opname uitgeschakeld) |

### Eindredenen

| Waarde | Betekenis |
|-------|---------|
| `user_hangup` | Externe partij heeft als eerste opgehangen |
| `ai_hangup` | AI heeft het gesprek bewust beëindigd |
| `ai_transfer` | AI heeft het gesprek doorverbonden; `transfer_number` is ingesteld |
| `ai_warm_transfer` | AI heeft een warme (begeleide) doorverbinding voltooid |
| `voicemail_hangup` | Voicemail is gedetecteerd en het gesprek is beëindigd volgens je `voicemail_action` |
| `max_duration` | Het gesprek heeft de maximale duur bereikt: de limiet van de agent, de limiet van het gesprek zelf of de platformlimiet van 6 uur. Een gesprek dat de AI of de beller na de waarschuwing voor de tijdslimiet beëindigt, behoudt `ai_hangup` of `user_hangup` |
| `superseded` | De sessie is vervangen door een nieuwere |
| `inbound_config_webhook_failed` | Inkomend gesprek naar een nummer in webhookmodus: de [configuratiewebhook](/nl/webhooks/call-incoming) heeft geen bruikbare configuratie geretourneerd, waardoor het gesprek is beëindigd voordat een agent opnam. `status` is `failed` en het gesprek wordt niet gefactureerd |
| `unknown` | De reden voor beëindiging kon niet worden vastgesteld |

## Transcriptindeling

Elke invoer in `transcripts` is één gespreksbeurt. Rollen zijn
`user` (spraak van de beller), `model` (spraak van de agent **en** toolaanroepen),
`tool` (toolresultaten) en `system` (oproepgebeurtenissen zoals taalwisselingen).

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

| Veld | Type | Beschrijving |
|-------|------|-------------|
| `role` | string | `user`, `model`, `tool` of `system` |
| `content_type` | string | `text/plain` voor spraak; `application/json` voor toolaanroepen, toolresultaten en systeemgebeurtenissen |
| `content` | string \| object | Spraaktekst of het hierboven weergegeven gestructureerde object. Toolaanroepen: `{"tool_call": name, "arguments": {…}}`. Toolresultaten: `{"tool_name": name, "response": {…}}` |
| `start_ms`, `end_ms` | integer | Verschuivingen vanaf het begin van de oproep, in ms. Aanwezig wanneer audiotiming bekend is |
| `ttfa_ms` | integer | Tijd tot de eerste audio voor een `model`-beurt, indien gemeten |
| `audio_url`, `audio_urls` | string / array | Verlopende ondertekende URL's voor de audio van de beurt, indien per beurt opgenomen |

Gebruik voor de volledig gestructureerde beurtgeschiedenis (met onderbrekingsmarkeringen,
bevestigingsprompts en onbewerkte posities)
[`GET /v1/calls/{call_id}/history`](/api-reference/calls#get-history).

## Verschillen met de legacy-payload

De legacy webhook-envelope met één URL is
`{"type": "telephony.complete" | "web.complete", "data": {…}}` met
**geen `event_id`**, en de `data` wijkt af van de endpoint-payload:

De legacy voltooiingspayload bevat ook `agent_id` en `agent_name`.

* De beurtarray staat onder **`history`**, niet onder `transcripts` (hetzelfde beurtenschema
  als hierboven).
* De veldenverzameling is het onbewerkte eindrapport van de oproep en kan
  aanvullende interne velden bevatten naast de bovenstaande tabel — behandel onbekende
  velden als informatief.
* Weboproepen (`direction: "web"`) **laten** `from_number` / `to_number`
  weg en voegen `origin_domain` toe.
* Mic-testoproepen in de builder worden op het legacy-pad gerapporteerd als `telephony.complete`
  (het endpointsysteem wijst ze toe aan `web.complete`).
* **Overdrachtscoördinatie:** wanneer een oproep eindigt met een overdracht, wordt de
  legacy webhook synchroon aangeroepen en kan deze
  `{"transfer_ready": false}` retourneren om aan te geven dat het overdrachtsdoel nog niet
  gereed is. Elke andere respons (of geen legacy webhook) laat de overdracht
  doorgaan. Endpoint-leveringen worden hiervoor nooit geraadpleegd.

---

## Voorbeeldhandler

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

---

## Veelvoorkomende toepassingen

<CardGroup cols={2}>
  <Card title="CRM-integratie" icon="database">
    Sla het transcript en de opname-URL van elke oproep op naast je
    klantgegevens.
  </Card>
  <Card title="Analyse" icon="chart-line">
    Stream transcripties naar een pijplijn voor onderwerpenmodellering, extractie
    van CSAT-signalen of monitoring van overdrachtspercentages.
  </Card>
  <Card title="Kwaliteitscontrole" icon="clipboard-check">
    Open oproepen in een QA-tool voor menselijke beoordeling, of voer ze uit met
    je eigen evaluatiemodel.
  </Card>
  <Card title="Meldingen" icon="bell">
    Activeer een menselijk teamlid bij overdracht / storing.
  </Card>
</CardGroup>

---

## Gerelateerd

<CardGroup cols={2}>
  <Card title="telephony.incoming / web.incoming" icon="phone" href="/nl/webhooks/call-incoming">
    De blokkerende tegenhanger die wordt uitgevoerd bij het begin van een oproep.
  </Card>
  <Card title="Gebeurteniscatalogus" icon="list" href="/nl/webhooks/events">
    Andere gebeurtenistypen waarop je je kunt abonneren.
  </Card>
  <Card title="API voor oproepgeschiedenis" icon="phone" href="/api-reference/calls">
    Dezelfde gegevens zijn via REST toegankelijk voor aanvullen / opnieuw afspelen.
  </Card>
</CardGroup>
