telephony.complete / web.complete
Ikke-blokerende webhook leveret, når et opkald afsluttes, med transskription, URL til optagelse og målinger.
En fuldførelseshændelse udløses, efter hvert opkald slutter — indgående telefoni, udgående telefoni, webopkald eller testopkald (mikrofonsession i builderen). Den er ikke-blokerende: svar med en vilkårlig 2xx-statuskode.
Hændelsen leveres ad begge veje:
- Webhook-slutpunkter modtager
telephony.complete(telefonopkald) ellerweb.complete(webopkald og mikrofontestopkald i builderen) med den stabile payload, der er dokumenteret nedenfor, etevent_idpr. levering, en timeout på 30 sek. og genforsøg i op til 24 t.. - Den ældre webhook med én URL modtager ét synkront forsøg (timeout på 10 sek., ingen genforsøg) med en lidt anderledes payload — se Forskelle i ældre payload.
Request-payload (leveringer til slutpunkter)
{
"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"
}| Felt | Type | Beskrivelse |
|---|---|---|
call_id | integer | Stabil på tværs af alle hændelser for dette opkald |
direction | string | inbound, outbound, web, test. Historiske payloads kan indeholde de ældre værdier mic eller widget |
from_number, to_number | string | E.164. from_number er bogstaveligt "web" for webopkald og testopkald |
origin_domain | string | Kun web/test — sideoprindelsen, der hostede widgetten (tom for mikrofonsessioner) |
start_time, end_time | timestamp | ISO 8601 UTC |
duration_seconds | integer | null | Udledt fra start/slut |
status | string | completed eller failed |
end_reason | string | Se tabellen nedenfor |
product, voice | string | Agentkonfiguration, der var aktiv på opkaldstidspunktet |
transfer_number | string | null | Angives, når opkaldet blev viderestillet |
recording_url | string | null | Udløbende signeret URL; download straks. null, når intet optagelsesartefakt er tilgængeligt |
billable_minutes | number | Fakturerede minutter, afrundet til nærmeste kvarte minut (intervaller på 15 sekunder, minimum 0.25). Opkald, der går direkte til telefonsvarer, rapporterer stadig deres faktiske målte minutter her, men opkrævningen er begrænset til ét minut til abonnementsprisen. |
billing_total_cents | integer | Amerikanske cents |
transcripts | array | Transskriptposter pr. tur; kan være tomt, når et transskript ikke er tilgængeligt |
Årsager til afslutning
| Værdi | Betydning |
|---|---|
user_hangup | Modparten lagde på først |
ai_hangup | Agenten afsluttede opkaldet bevidst |
ai_transfer | Agenten viderestillede opkaldet; transfer_number er angivet |
ai_warm_transfer | Agenten fuldførte en varm (assisteret) viderestilling |
voicemail_hangup | Telefonsvarer blev registreret, og opkaldet blev afsluttet i henhold til din voicemail_action |
max_duration | Opkaldet nåede grænsen for maksimal varighed |
superseded | Sessionen blev erstattet af en nyere |
unknown | Årsagen til afslutningen kunne ikke fastslås |
Transkriptformat
Hvert element i transcripts er én samtaletur. Roller er
user (opkalderens tale), model (agentens tale og værktøjskald),
tool (værktøjsresultater) og system (opkaldshændelser såsom
sprogskift).
[
{
"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 værktøjskald, værktøjsresultater og systemhændelser |
content | streng | objekt | Taletekst eller det strukturerede objekt vist ovenfor. Værktøjskald: {"tool_call": name, "arguments": {…}}. Værktøjsresultater: {"tool_name": name, "response": {…}} |
start_ms, end_ms | heltal | Forskydninger fra opkaldsstart, ms. Findes, når lydtiming er kendt |
ttfa_ms | heltal | Tid til første lyd for en model-tur, når den er målt |
audio_url, audio_urls | streng / array | Udløbende signerede URL'er til turens lyd, når lyd optages pr. tur |
Brug den fuldt strukturerede turhistorik (med afbrydelsesmarkører,
bekræftelsesprompter og rå positioner) med
GET /v1/calls/{call_id}/history.
Forskelle i ældre payloads
Det ældre webhook-omslag med enkelt-URL er
{"type": "telephony.complete" | "web.complete", "data": {…}} uden
event_id, og dets data adskiller sig fra endpoint-payloadet:
- Turarrayet ligger under
history, ikketranscripts(samme turskema som ovenfor). - Feltsættet er den rå rapport ved opkaldets afslutning og kan omfatte yderligere interne felter ud over tabellen ovenfor — behandl ukendte felter som informative.
- Webopkald (
direction: "web") udeladerfrom_number/to_numberog tilføjerorigin_domain. - Builder-mikrofontestopkald rapporteres som
telephony.completepå den ældre sti (endpoint-systemet knytter dem tilweb.complete). - Overførselskoordinering: Når et opkald slutter med en overførsel,
kaldes den ældre webhook synkront og kan svare
{"transfer_ready": false}for at signalere, at overførselsmålet ikke er klar. Ethvert andet svar (eller ingen ældre webhook) lader overførslen fortsætte. Endpoint-leveringer konsulteres aldrig til dette.
Eksempel på handler
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}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 });
},
);Almindelige anvendelsesområder
Gem hvert opkalds transskription og URL til optagelse sammen med dine kundeposter.
Stream transskriptioner til en pipeline til emnemodellering, udtræk af CSAT-signaler eller overvågning af viderestillingsrate.
Åbn opkald i et QA-værktøj til menneskelig gennemgang, eller kør dem gennem din egen evalueringsmodel.
Underret en menneskelig kollega ved viderestilling eller fejl.