ThunderPhone 2.0 ist live.Direkt im Self-Service – ab 2 ¢/Min..Ankündigung lesen

Webhooks

telephony.complete / web.complete

Nicht blockierender Webhook, der beim Ende eines Anrufs mit Transkript, Aufzeichnungs-URL und Metriken gesendet wird.

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

Das Ereignis wird über beide Wege zugestellt:

Request-Nutzlast (Endpunktzustellungen)

{
  "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",
    "from_number": "+14155550199",
    "product": "spark",
    "recording_url": "https://storage.example.com/…",
    "start_time": "2026-04-20T18:24:10.113Z",
    "status": "completed",
    "to_number": "+15551234567",
    "transcripts": [ /* see Transcript format */ ],
    "transfer_number": null,
    "voice": "john"
  },
  "event_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
  "type": "telephony.complete"
}
FeldTypBeschreibung
call_idintegerStabil über jedes Ereignis für diesen Anruf hinweg
directionstringinbound, outbound, web, test. Historische Nutzlasten können die veralteten Werte mic oder widget enthalten
from_number, to_numberstringE.164. from_number ist bei Web-Anrufen und Testanrufen der Literalwert "web"
origin_domainstringNur Web/Test — der Seitenursprung, der das Widget gehostet hat (bei Mikrofonsitzungen leer)
start_time, end_timetimestampISO 8601 UTC
duration_secondsinteger | nullAus Start-/Endzeit abgeleitet
statusstringcompleted oder failed
end_reasonstringSiehe Tabelle unten
product, voicestringZum Zeitpunkt des Anrufs wirksame Agentenkonfiguration
transfer_numberstring | nullWird gesetzt, wenn der Anruf weitergeleitet wurde
recording_urlstring | nullSignierte URL mit Ablaufzeit; laden Sie sie zeitnah herunter. null, wenn kein Aufzeichnungsartefakt verfügbar ist
billable_minutesnumberAbgerechnete Minuten, auf die nächste Viertelminute gerundet (15-Sekunden-Schritte, mindestens 0,25). Anrufe, die direkt zur Mailbox gehen, melden hier weiterhin ihre tatsächlich gemessenen Minuten, die Gebühr ist jedoch auf eine Minute zum Tarif des Plans begrenzt.
billing_total_centsintegerUS-Cent
transcriptsarrayTranskripteinträge pro Gesprächszug; kann leer sein, wenn kein Transkript verfügbar ist

Endgründe

WertBedeutung
user_hangupDie andere Gesprächspartei hat zuerst aufgelegt
ai_hangupDie KI hat den Anruf absichtlich beendet
ai_transferDie KI hat den Anruf weitergeleitet; transfer_number ist gesetzt
ai_warm_transferDie KI hat eine warme (betreute) Weiterleitung abgeschlossen
voicemail_hangupEine Mailbox wurde erkannt und der Anruf gemäß Ihrer voicemail_action beendet
max_durationDer Anruf hat das maximale Dauerlimit erreicht
supersededDie Sitzung wurde durch eine neuere ersetzt
unknownDer Endgrund konnte nicht bestimmt werden

Transkriptformat

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

[
  {
    "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"] }
    }
  }
]
FeldTypBeschreibung
rolestringuser, model, tool oder system
content_typestringtext/plain für Sprache; application/json für Tool-Aufrufe, Tool-Ergebnisse und Systemereignisse
contentstring | objectSprachtext oder das oben dargestellte strukturierte Objekt. Tool-Aufrufe: {"tool_call": name, "arguments": {…}}. Tool-Ergebnisse: {"tool_name": name, "response": {…}}
start_ms, end_msintegerOffsets ab Anrufbeginn in ms. Vorhanden, wenn Audio-Timing bekannt ist
ttfa_msintegerZeit bis zum ersten Audio für einen model-Beitrag, sofern gemessen
audio_url, audio_urlsstring / arrayAblaufende signierte URLs für das Audio des Beitrags, sofern pro Beitrag aufgezeichnet

Verwenden Sie für den vollständig strukturierten Gesprächsverlauf (mit Unterbrechungsmarkierungen, Bestätigungsaufforderungen und Rohpositionen) GET /v1/calls/{call_id}/history.

Unterschiede bei Legacy-Payloads

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

  • Das Beitragsarray befindet sich unter history, nicht unter transcripts (dasselbe Beitragsschema wie oben).
  • Die Feldmenge ist der Rohbericht am Anrufende und kann zusätzliche interne Felder über die obige Tabelle hinaus enthalten — behandeln Sie unbekannte Felder als informativ.
  • Webanrufe (direction: "web") lassen from_number / to_number weg und fügen origin_domain hinzu.
  • Builder-Mikrofontestanrufe werden auf dem 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 mit {"transfer_ready": false} antworten, um zu signalisieren, dass das Übergabeziel noch nicht bereit ist. Jede andere Antwort (oder kein Legacy-Webhook) lässt den Transfer fortfahren. Endpoint-Zustellungen werden dafür niemals berücksichtigt.

Beispiel-Handler

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}
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 });
  },
);

Häufige Anwendungsfälle

CRM-Integration

Speichern Sie das Transkript und die Aufzeichnungs-URL jedes Anrufs zusammen mit Ihren Kundendaten.

Analysen

Streamen Sie Transkripte in eine Pipeline für Themenmodellierung, die Extraktion von CSAT-Signalen oder die Überwachung der Weiterleitungsrate.

Qualitätsprüfung

Öffnen Sie Anrufe zur manuellen Prüfung in einem QA-Tool oder lassen Sie sie durch Ihr eigenes Bewertungsmodell auswerten.

Benachrichtigungen

Benachrichtigen Sie bei Weiterleitung oder Fehler ein menschliches Teammitglied.


Weiterführend