---
title: "telephony.complete / web.complete"
description: "Nicht blockierender Webhook, der beim Ende eines Anrufs mit Transkript, Aufzeichnungs-URL und Metriken bereitgestellt wird."
---

Ein Abschlussereignis wird ausgelöst, nachdem jeder Anruf endet — eingehende Telefonie,
ausgehende Telefonie, Webanruf oder Testanruf (Builder-Mikrofonsitzung). Es ist
**nicht blockierend**: Antworten Sie mit einem beliebigen 2xx-Status.

Das Ereignis wird über beide Pfade zugestellt:

* **[Webhook-Endpunkte](/de/webhooks/endpoints)** empfangen
  `telephony.complete` (Telefonanrufe) oder `web.complete` (Webanrufe und
  Builder-Mikrofon-Testanrufe) mit der unten dokumentierten stabilen Nutzlast,
  einer `event_id` pro Zustellung, einem Timeout von 30 s und
  [Wiederholungen für bis zu 24 h](/de/webhooks/overview).
* **Der [Legacy-Single-URL-Webhook](/api-reference/organizations#legacy-single-url-webhook)**
  erhält einen synchronen Versuch (Timeout von 10 s, keine Wiederholungen) mit einer
  leicht abweichenden Nutzlast — siehe
  [Unterschiede bei der Legacy-Nutzlast](#legacy-payload-differences).

## Anforderungsnutzlast (Endpunkt-Zustellungen)

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

| Feld | Typ | Beschreibung |
|-------|------|-------------|
| `call_id` | integer | Über alle Ereignisse für diesen Anruf hinweg stabil |
| `agent_id` | integer \| null | Agent, der den Anruf bearbeitet hat, sofern einer zugewiesen war |
| `agent_name` | string \| null | Agent, der den Anruf bearbeitet hat, sofern einer zugewiesen war |
| `direction` | string | `inbound`, `outbound`, `web`, `test`. Historische Payloads können die veralteten Werte `mic` oder `widget` enthalten |
| `from_number`, `to_number` | string | E.164. `from_number` ist bei Webanrufen und Testanrufen wörtlich `"web"` |
| `origin_domain` | string | **Nur Web/Test** — der Seitenursprung, der das Widget gehostet hat (bei Mic-Sitzungen leer) |
| `start_time`, `end_time` | timestamp | ISO 8601 UTC |
| `duration_seconds` | integer \| null | Aus Start- und Endzeit abgeleitet |
| `status` | string | `completed` oder `failed` |
| `end_reason` | string | Siehe Tabelle unten |
| `product`, `voice` | string | Zum Zeitpunkt des Anrufs wirksame Agentenkonfiguration |
| `variables` | object | Beim Start des Anrufs erfasste Eingabevariablen |
| `unresolved_variables` | array | Variablennamen, auf die die Anrufkonfiguration verweist, die aber beim Anrufstart nicht bereitgestellt wurden |
| `transfer_number` | string \| null | Wird gesetzt, wenn der Anruf weitergeleitet wurde |
| `recording_url` | string \| null | Ablaufende signierte URL; laden Sie sie zeitnah herunter. `null`, wenn kein Aufzeichnungsartefakt verfügbar ist |
| `recording_stopped_at` | timestamp \| null | Zeitpunkt, zu dem die Aufzeichnung vor dem Ende des Anrufs beendet wurde; nach diesem Zeitpunkt ist nichts mehr in der Aufzeichnung enthalten. `null`, wenn die Aufzeichnung bis zum Ende des Anrufs lief oder der Anruf nicht aufgezeichnet wurde |
| `recording_stopped_reason` | string \| null | Grund, warum die Aufzeichnung vorzeitig beendet wurde. `warm_transfer`: Sie wird unmittelbar vor dem Anruf einer [Warmübergabe](/de/guides/call-transfers) an das Ziel beendet und bleibt deaktiviert, auch wenn die Weiterleitung fehlschlägt und der Anrufer zum Agenten zurückkehrt. Behandeln Sie andere Werte als Informationen; weitere können hinzugefügt werden. `null`, wenn `recording_stopped_at` `null` ist |
| `billable_minutes` | number | Abgerechnete Minuten, auf die nächste Viertelminute gerundet (15-Sekunden-Schritte, mindestens 0,25). Anrufe, die direkt auf der Mailbox landen, geben hier weiterhin ihre tatsächlich gemessenen Minuten an, die Gebühr ist jedoch auf eine Minute zum Plantarif begrenzt. |
| `billing_total_cents` | integer | US-Cent |
| `transcripts` | array | Transkripteinträge pro Gesprächszug; kann leer sein, wenn kein Transkript verfügbar ist |
| `summary` | string \| null | Verständliche Zusammenfassung nach dem Anruf (zwei bis vier Prosasätze, höchstens 2.000 Zeichen, in der Sprache des Anrufs: der primären Sprache des Agenten, sofern der Anruf sie nicht überschrieben hat), wenn der Agent im Agent Builder unter **Zu erhebende Daten** **Anrufzusammenfassung generieren** aktiviert hat (`call_summary_enabled` beim [Agenten](/api-reference/agents)) und die Generierung erfolgreich war; andernfalls `null`, auch bei Anrufen, die zu kurz für eine Zusammenfassung sind, Anrufen ohne Transkript und Agenten mit deaktivierter Aufzeichnung. Die Zusammenfassung wird im selben Schritt nach dem Anruf wie die Extraktion generiert. Daher entscheidet die Einstellung **Vor dem Senden des Webhooks „Anruf beendet“ auf extrahierte Daten warten** (`extraction_blocks_completion`), ob dieses Ereignis darauf wartet: In einem Wartemodus ist die Zusammenfassung fertig, bevor dieses Ereignis gesendet wird (die Zusammenfassung selbst erhält innerhalb des 75-Sekunden-Budgets des Schritts höchstens 30 Sekunden); wenn das Ereignis nicht wartet (`never` oder eine Weiterleitung unter `except_transfers`), ist dieses Feld `null` und die Zusammenfassung wird nicht erneut in `call.data_extracted` gesendet. Lesen Sie sie daher später aus `summary` im [Anrufobjekt](/api-reference/calls#call-object). Auch auf der Anrufseite im Dashboard verfügbar |
| `extracted_data` | object \| null | Strukturiertes Extraktionsergebnis mit `status`, `fields`, `evidence`, `verification`, `field_reasons` und `schema_version`. Jedes Feld mit einem Wert ungleich null enthält das exakt strukturell geprüfte Zitat (höchstens 1.000 Zeichen) sowie Sprecherrolle und Gesprächszugindex; längere vom Modell zurückgegebene Zitate werden abgelehnt statt gekürzt. Evidenz ist `null`, wenn das Feld `null` ist. `verified` bedeutet, dass jeder Kandidat genau ein gültiges unabhängiges Urteil erhalten hat. `unavailable` umfasst auch fehlerhafte oder unvollständige Verifier-Ausgaben; gültige Teilurteile werden weiterhin angewendet, während Kandidaten ohne ein gültiges Urteil auf null gesetzt werden. `status` ist `completed`, `failed`, `exhausted`, `skipped` oder `skipped_recording_disabled`; `null`, wenn der Agent keine Extraktionsfelder hatte |
| `steps_trace` | array | Nur bei Anrufen, deren Agent Schritte oder Übergaben hat. Die Reihenfolge, in der der Anruf sie durchlaufen hat: Jeder Eintrag enthält `t_ms` (Millisekunden seit Anrufbeginn), `agent` (den Schlüssel des Agenten, `a` plus seine ID), `step` (die Schritt-ID oder `null` bei einer Übergabe an einen Agenten ohne Schritte) und `via` (`start`, `model`, `condition`, `on_enter` oder `handoff`). Enthält nur IDs und Zeiten und wird daher auch bei deaktivierter Aufzeichnung beibehalten |
| `collected_values` | object | Nur bei Anrufen, deren Agent Schritte oder Übergaben hat. Die Werte, die der Agent in seinen Schritten erfasst hat, nach dem Namen des Erfassungsfelds gruppiert, sowie `<tool>_result` für das Ergebnis von `on_enter_tool` jedes Schritts. Wird ausgelassen, wenn der Inhalt des Anrufs nicht gespeichert wird (Aufzeichnung deaktiviert) |

### Endgründe

| Wert | Bedeutung |
|-------|---------|
| `user_hangup` | Die Gegenpartei hat zuerst aufgelegt |
| `ai_hangup` | Die KI hat den Anruf absichtlich beendet |
| `ai_transfer` | Die KI hat den Anruf weitergeleitet; `transfer_number` ist gesetzt |
| `ai_warm_transfer` | Die KI hat eine betreute Weiterleitung abgeschlossen |
| `voicemail_hangup` | Eine Mailbox wurde erkannt und der Anruf gemäß Ihrer `voicemail_action` beendet |
| `max_duration` | Der Anruf hat die maximale Dauer erreicht: das Limit des Agenten, das eigene Limit des Anrufs oder das Plattformlimit von 6 Stunden. Ein Anruf, den die KI oder der Anrufer nach der Warnung zum Zeitlimit beendet, behält `ai_hangup` oder `user_hangup` bei |
| `superseded` | Die Sitzung wurde durch eine neuere ersetzt |
| `inbound_config_webhook_failed` | Eingehender Anruf an eine Nummer im Webhook-Modus: Der [Konfigurations-Webhook](/de/webhooks/call-incoming) hat keine verwendbare Konfiguration zurückgegeben, daher wurde der Anruf beendet, bevor ein Agent antwortete. `status` ist `failed` und der Anruf wird nicht abgerechnet |
| `unknown` | Der Grund für die Beendigung konnte nicht ermittelt werden |

## Transkriptformat

Jeder Eintrag in `transcripts` ist ein Gesprächszug. Rollen sind
`user` (Sprache des Anrufers), `model` (Sprache des Sprachagenten **und** Tool-Aufrufe),
`tool` (Tool-Ergebnisse) und `system` (Anrufereignisse wie
Sprachwechsel).

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

| Feld | Typ | Beschreibung |
|-------|------|-------------|
| `role` | String | `user`, `model`, `tool` oder `system` |
| `content_type` | String | `text/plain` für Sprache; `application/json` für Tool-Aufrufe, Tool-Ergebnisse und Systemereignisse |
| `content` | String \| Objekt | Sprachtext oder das oben gezeigte strukturierte Objekt. Tool-Aufrufe: `{"tool_call": name, "arguments": {…}}`. Tool-Ergebnisse: `{"tool_name": name, "response": {…}}` |
| `start_ms`, `end_ms` | Ganzzahl | Versätze ab Anrufbeginn in ms. Vorhanden, wenn Audio-Timing bekannt ist |
| `ttfa_ms` | Ganzzahl | Zeit bis zum ersten Audio eines `model`-Zugs, sofern gemessen |
| `audio_url`, `audio_urls` | String / Array | Ablaufende signierte URLs für das Audio des Zugs, wenn pro Zug aufgezeichnet |

Verwenden Sie für den vollständig strukturierten Zugverlauf (mit Unterbrechungsmarkierungen,
Bestätigungsaufforderungen und Rohpositionen)
[`GET /v1/calls/{call_id}/history`](/api-reference/calls#get-history).

## Unterschiede bei Legacy-Payloads

Das Legacy-Webhook-Format mit einer einzelnen URL lautet
`{"type": "telephony.complete" | "web.complete", "data": {…}}` und enthält
**keine `event_id`**; sein `data` unterscheidet sich von der Endpoint-Payload:

Die Legacy-Abschluss-Payload enthält außerdem `agent_id` und `agent_name`.

* Das Zug-Array befindet sich unter **`history`**, nicht unter `transcripts` (gleiches
  Zug-Schema wie oben).
* Die Feldmenge ist der rohe Bericht zum Anrufende und kann
  zusätzliche interne Felder über die obige Tabelle hinaus enthalten — behandeln Sie unbekannte
  Felder als Informationen.
* Web-Anrufe (`direction: "web"`) **lassen** `from_number` / `to_number`
  weg und fügen `origin_domain` hinzu.
* Builder-Mikrofontest-Anrufe werden im Legacy-Pfad als `telephony.complete`
  gemeldet (das Endpoint-System ordnet sie `web.complete` zu).
* **Transferkoordination:** Wenn ein Anruf während eines Transfers endet, wird der
  Legacy-Webhook synchron aufgerufen und kann
  `{"transfer_ready": false}` zurückgeben, um zu signalisieren, dass das Übergabeziel nicht
  bereit ist. Jede andere Antwort (oder kein Legacy-Webhook) ermöglicht die Fortsetzung des Transfers.
  Endpoint-Zustellungen werden hierfür niemals berücksichtigt.

---

## Beispiel-Handler

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

---

## Häufige Anwendungsfälle

<CardGroup cols={2}>
  <Card title="CRM-Integration" icon="database">
    Speichern Sie das Transkript und die Aufzeichnungs-URL jedes Anrufs zusammen mit Ihren
    Kundendaten.
  </Card>
  <Card title="Analysen" icon="chart-line">
    Streamen Sie Transkripte an eine Pipeline für Themenmodellierung, die Extraktion von
    CSAT-Signalen oder die Überwachung der Weiterleitungsrate.
  </Card>
  <Card title="Qualitätsprüfung" icon="clipboard-check">
    Öffnen Sie Anrufe zur manuellen Prüfung in einem QA-Tool oder lassen Sie sie durch Ihr
    eigenes Bewertungsmodell laufen.
  </Card>
  <Card title="Benachrichtigungen" icon="bell">
    Benachrichtigen Sie bei Weiterleitung oder Fehler einen menschlichen Teamkollegen.
  </Card>
</CardGroup>

---

## Verwandte Inhalte

<CardGroup cols={2}>
  <Card title="telephony.incoming / web.incoming" icon="phone" href="/de/webhooks/call-incoming">
    Das blockierende Gegenstück, das beim Anrufbeginn ausgeführt wird.
  </Card>
  <Card title="Ereigniskatalog" icon="list" href="/de/webhooks/events">
    Weitere Ereignistypen, die Sie abonnieren können.
  </Card>
  <Card title="Anrufverlauf-API" icon="phone" href="/api-reference/calls">
    Dieselben Daten sind über REST für Nachfüllung und Wiederholung verfügbar.
  </Card>
</CardGroup>
