Open in
telephony.complete / web.complete
Ikke-blokerende webhook leveret, når et opkald afsluttes, med transskription, optagelses-URL og metrics.
En fuldførelseshændelse udløses, når hvert opkald afsluttes — 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 via begge stier:
- Webhook-slutpunkter modtager
telephony.complete(telefonopkald) ellerweb.complete(webopkald og mikrofonsessioner til test i builderen) med den stabile payload, der er dokumenteret nedenfor, etevent_idpr. levering, en timeout på 30 s og genforsøg i op til 24 t. - Den ældre webhook med én URL modtager ét synkront forsøg (timeout på 10 s, ingen genforsøg) med en lidt anderledes payload — se Forskelle i ældre payload.
Anmodningsnyttelast (endpoint-leveringer)
{
"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 | heltal | Stabil på tværs af alle hændelser for dette opkald |
agent_id | heltal | null | Agent, der håndterede opkaldet, når en var tildelt |
agent_name | streng | null | Agent, der håndterede opkaldet, når en var tildelt |
direction | streng | inbound, outbound, web, test. Historiske payloads kan indeholde de ældre værdier mic eller widget |
from_number, to_number | streng | E.164. from_number er den bogstavelige værdi "web" for webopkald og testopkald |
origin_domain | streng | Kun web/test — sidens oprindelse, som hostede widgetten (tom for mic-sessioner) |
start_time, end_time | tidsstempel | ISO 8601 UTC |
duration_seconds | heltal | null | Udledt af start/slut |
status | streng | completed eller failed |
end_reason | streng | Se tabellen nedenfor |
product, voice | streng | Agentkonfiguration, der var aktiv på opkaldstidspunktet |
variables | objekt | Inputvariabler som øjebliksbillede, da opkaldet startede |
unresolved_variables | array | Variabelnavne, der refereres til af opkaldskonfigurationen, men ikke blev leveret ved opkaldets start |
transfer_number | streng | null | Angives, når opkaldet blev viderestillet |
recording_url | streng | null | Udløbende signeret URL; download straks. null, når intet optagelsesartefakt er tilgængeligt |
recording_stopped_at | tidsstempel | null | Tidspunktet, hvor optagelsen blev stoppet, før opkaldet sluttede; intet efter dette tidspunkt er med i optagelsen. null, når optagelsen fortsatte til opkaldets slutning, eller opkaldet ikke blev optaget |
recording_stopped_reason | streng | null | Hvorfor optagelsen stoppede tidligt. warm_transfer: den stopper lige før en varm viderestilling ringer til destinationen og forbliver slået fra, selv hvis viderestillingen mislykkes, og opkalderen vender tilbage til agenten. Behandl andre værdier som informative; der kan tilføjes flere. null, når recording_stopped_at er null |
billable_minutes | tal | Fakturerede minutter, afrundet til nærmeste kvarte minut (15-sekunders intervaller, minimum 0.25). Opkald, der går direkte til telefonsvarer, rapporterer stadig deres faktiske målte minutter her, men beløbet er begrænset til ét minut til planens takst. |
billing_total_cents | heltal | USD-cent |
transcripts | array | Transskriptposter pr. tur; kan være tom, når et transskript ikke er tilgængeligt |
summary | streng | null | Letforståelig oversigt efter opkaldet (to til fire sætninger, højst 2.000 tegn, skrevet på opkaldets sprog: agentens primære sprog, medmindre opkaldet tilsidesatte det), når agenten har Generer en opkaldsoversigt slået til under Data, der skal indsamles i agentbyggeren (call_summary_enabled på Agent), og genereringen lykkedes; ellers null, herunder for opkald, der er for korte til at opsummere, opkald uden transskript og agenter med optagelse slået fra. Oversigten genereres i samme trin efter opkaldet som udtrækningen, så indstillingen Vent på udtrukne data, før webhooken for afsluttet opkald sendes (extraction_blocks_completion) afgør, om denne hændelse venter på den: I en ventetilstand er oversigten klar, før denne hændelse sendes (selve oversigten får højst 30 sekunder inden for trinnets budget på 75 sekunder); når hændelsen ikke venter (never eller en viderestilling under except_transfers), er dette felt null, og oversigten sendes ikke igen i call.data_extracted, så læs den senere fra summary på Call-objektet. Også tilgængelig på opkaldssiden i dashboardet |
extracted_data | objekt | null | Struktureret udtrækningsresultat med status, fields, evidence, verification, field_reasons og schema_version. Hvert felt, der ikke er null, har det nøjagtige strukturelt kontrollerede citat (højst 1.000 tegn) samt talerrolle og turindeks; længere citater returneret af modellen afvises i stedet for at blive afkortet. Evidens er null, når feltet er null. verified betyder, at hver kandidat modtog præcis én gyldig uafhængig vurdering. unavailable dækker også ukorrekt formateret eller delvist output fra verificeringen; gyldige delvise vurderinger anvendes stadig, mens kandidater uden én gyldig vurdering sættes til null. status er completed, failed, exhausted, skipped eller skipped_recording_disabled; null, når agenten ikke havde nogen udtrækningsfelter |
steps_trace | array | Kun på opkald, hvis agent har trin eller overleveringer. Rækkefølgen, som opkaldet bevægede sig gennem dem i: Hver post har t_ms (millisekunder siden opkaldet startede), agent (agentens nøgle, a plus dens ID), step (trin-ID'et eller null ved en overlevering til en agent uden trin) og via (start, model, condition, on_enter eller handoff). Indeholder kun ID'er og tidspunkter, så den bevares også, når optagelse er slået fra |
collected_values | objekt | Kun på opkald, hvis agent har trin eller overleveringer. De værdier, agenten indsamlede i sine trin, med navn på indsamlingsfeltet som nøgle, samt <tool>_result for hvert trins on_enter_tool-resultat. Udelades, når opkaldets indhold ikke bevares (optagelse slået fra) |
Slutårsager
| Værdi | Betydning |
|---|---|
user_hangup | Den eksterne part lagde på først |
ai_hangup | AI'en afsluttede opkaldet med vilje |
ai_transfer | AI'en viderestillede opkaldet; transfer_number er angivet |
ai_warm_transfer | AI'en gennemfø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 den maksimale varighedsgrænse: agentens eller opkaldets egen grænse eller platformens grænse på 6 timer. Et opkald, som AI'en eller den, der ringer, afslutter efter advarslen om tidsgrænsen, beholder ai_hangup eller user_hangup |
superseded | Sessionen blev erstattet af en nyere |
inbound_config_webhook_failed | Indgående opkald til et nummer i webhooktilstand: konfigurationswebhooken returnerede ikke en brugbar konfiguration, så opkaldet blev afsluttet, før en agent svarede. status er failed, og opkaldet faktureres ikke |
unknown | Afslutningsårsagen kunne ikke fastslås |
Transskriptformat
Hvert element i transcripts er én samtalevending. Roller er
user (opkalderens tale), model (agentens tale og værktøjskald),
tool (værktøjsresultater) og system (opkaldshændelser 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 opkaldets start, ms. Findes, når lydtiming er kendt |
ttfa_ms | heltal | Tid til første lyd for en model-vending, når den er målt |
audio_url, audio_urls | streng / array | Udløbende signerede URL'er til vendingens lyd, når den optages pr. vending |
Brug den fuldt strukturerede vendinghistorik (med afbrydelsesmarkører,
bekræftelsesprompter og rå positioner) via
GET /v1/calls/{call_id}/history.
Forskelle i ældre payloads
Den ældre webhook-konvolut med én URL er
{"type": "telephony.complete" | "web.complete", "data": {…}} uden
event_id, og dens data adskiller sig fra endpointets payload:
Det ældre fuldførelsespayload indeholder også agent_id og agent_name.
- Vendingarrayet findes under
history, ikketranscripts(samme vendingsskema som ovenfor). - Feltsættet er den rå rapport ved opkaldets afslutning og kan indeholde yderligere interne felter ud over tabellen ovenfor — behandl ukendte felter som information.
- Webopkald (
direction: "web") udeladerfrom_number/to_numberog tilføjerorigin_domain. - Builder-mikrofontestopkald rapporteres som
telephony.completepå den ældre sti (endpointsystemet knytter dem tilweb.complete). - Overførselskoordinering: Når et opkald afsluttes i 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. Endpointleveringer konsulteres aldrig i dette tilfælde.
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 optagelses-URL sammen med dine kunderegistre.
Stream transskriptioner til en pipeline til emnemodellering, udtrækning af CSAT-signaler eller overvågning af overførselsrater.
Åbn opkald i et QA-værktøj til menneskelig gennemgang, eller kør dem gennem din egen evalueringsmodel.
Underret en menneskelig kollega ved overførsel eller fejl.