Open in
telephony.complete / web.complete
Icke-blockerande webhook som skickas när ett samtal avslutas, med transkription, inspelnings-URL och mätvärden.
En slutförandehändelse utlöses efter att varje samtal avslutas — inkommande telefoni, utgående telefoni, webbsamtal eller testsamtal (byggarens mikrofonsession). Den är icke-blockerande: svara med valfri 2xx.
Händelsen levereras via båda vägarna:
- Webhook-slutpunkter tar emot
telephony.complete(telefonsamtal) ellerweb.complete(webbsamtal och byggarens mikrofontestsamtal) med den stabila nyttolasten som dokumenteras nedan, ettevent_idper leverans, en timeout på 30 s och återförsök i upp till 24 h. - Den äldre webhooken med en enda URL tar emot ett synkront försök (timeout på 10 s, inga återförsök) med en något annorlunda nyttolast — se Skillnader i äldre nyttolast.
Begärandenyttolast (endpointleveranser)
{
"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"
}| Fält | Typ | Beskrivning |
|---|---|---|
call_id | heltal | Stabilt för varje händelse för detta samtal |
agent_id | heltal | null | Agent som hanterade samtalet, när en tilldelades |
agent_name | sträng | null | Agent som hanterade samtalet, när en tilldelades |
direction | sträng | inbound, outbound, web, test. Historiska nyttolaster kan innehålla äldre värden för mic eller widget |
from_number, to_number | sträng | E.164. from_number är den bokstavliga strängen "web" för webbsamtal och testsamtal |
origin_domain | sträng | Endast webb/test — sidans ursprung som var värd för widgeten (tomt för mikrofonsessioner) |
start_time, end_time | tidsstämpel | ISO 8601 UTC |
duration_seconds | heltal | null | Härleds från start/slut |
status | sträng | completed eller failed |
end_reason | sträng | Se tabellen nedan |
product, voice | sträng | Agentkonfiguration som gällde vid samtalstillfället |
variables | objekt | Indatavariabler som sparades som ögonblicksbild när samtalet startade |
unresolved_variables | matris | Variabelnamn som refereras av samtalskonfigurationen men inte angavs när samtalet startade |
transfer_number | sträng | null | Anges när samtalet överfördes |
recording_url | sträng | null | Tidsbegränsad signerad URL; ladda ned den omgående. null när ingen inspelningsartefakt är tillgänglig |
recording_stopped_at | tidsstämpel | null | När inspelningen stoppades innan samtalet avslutades; inget efter denna tidpunkt finns med i inspelningen. null när inspelningen fortsatte till slutet av samtalet eller samtalet inte spelades in |
recording_stopped_reason | sträng | null | Varför inspelningen stoppades i förtid. warm_transfer: den stoppas precis innan en varm överföring ringer destinationen och förblir avstängd även om överföringen misslyckas och uppringaren återvänder till agenten. Behandla andra värden som informativa; fler kan läggas till. null när recording_stopped_at är null |
billable_minutes | tal | Debiterade minuter, avrundade till närmaste kvarts minut (intervall på 15 sekunder, minst 0.25). Samtal som går direkt till röstbrevlådan rapporterar fortfarande sina faktiska uppmätta minuter här, men avgiften är begränsad till en minut enligt planens pris. |
billing_total_cents | heltal | Amerikanska cent |
transcripts | matris | Transkriptposter per tur; kan vara tom när ett transkript inte är tillgängligt |
summary | sträng | null | Sammanfattning efter samtalet på klarspråk (två till fyra meningar löpande text, högst 2 000 tecken, skriven på samtalets språk: agentens primära språk om inte samtalet åsidosatte det) när agenten har Generera en samtalssammanfattning aktiverat under Data att samla in i agentbyggaren (call_summary_enabled på Agent) och genereringen lyckades; annars null, inklusive för samtal som är för korta för att sammanfatta, samtal utan transkript och agenter med inspelning avstängd. Sammanfattningen genereras i samma steg efter samtalet som extraheringen, så inställningen Vänta på extraherade data innan webhooken för avslutat samtal skickas (extraction_blocks_completion) avgör om denna händelse väntar på den: i ett vänteläge är sammanfattningen klar innan denna händelse skickas (sammanfattningen får högst 30 sekunder inom stegets budget på 75 sekunder); när händelsen inte väntar (never eller en överföring under except_transfers) är detta fält null och sammanfattningen skickas inte på nytt i call.data_extracted, så läs den senare från summary i Call-objektet. Finns även på samtalssidan i dashboarden |
extracted_data | objekt | null | Strukturerat extraheringsresultat med status, fields, evidence, verification, field_reasons och schema_version. Varje fält som inte är null har det exakta strukturellt kontrollerade citatet (högst 1 000 tecken), samt talarroll och turindex; längre citat som returneras av modellen avvisas i stället för att trunkeras. Evidens är null när fältet är null. verified innebär att varje kandidat fick exakt ett giltigt oberoende utlåtande. unavailable omfattar även felaktiga eller partiella verifierarutdata; giltiga partiella utlåtanden tillämpas fortfarande, medan kandidater utan ett giltigt utlåtande sätts till null. status är completed, failed, exhausted, skipped eller skipped_recording_disabled; null när agenten inte hade några extraheringsfält |
steps_trace | matris | Endast för samtal vars agent har steg eller överlämningar. Ordningen som samtalet rörde sig genom dem: varje post har t_ms (millisekunder sedan samtalet startade), agent (agentens nyckel, a plus dess ID), step (steg-ID:t, eller null vid en överlämning till en agent utan steg) och via (start, model, condition, on_enter eller handoff). Innehåller endast ID:n och tider, så den behålls även när inspelning är avstängd |
collected_values | objekt | Endast för samtal vars agent har steg eller överlämningar. Värdena som agenten samlade in i sina steg, nycklade efter insamlingsfältnamn, plus <tool>_result för varje stegs on_enter_tool-resultat. Utelämnas när samtalets innehåll inte behålls (inspelning avstängd) |
Avslutsorsaker
| Värde | Betydelse |
|---|---|
user_hangup | Motparten lade på först |
ai_hangup | AI:n avslutade samtalet avsiktligt |
ai_transfer | AI:n kopplade samtalet vidare; transfer_number är angivet |
ai_warm_transfer | AI:n slutförde en varm (övervakad) vidarekoppling |
voicemail_hangup | Röstbrevlåda upptäcktes och samtalet avslutades enligt din voicemail_action |
max_duration | Samtalet nådde gränsen för maximal längd: agentens eller samtalets egen gräns, eller plattformens gräns på 6 timmar. Ett samtal som AI:n eller uppringaren avslutar efter varningen om tidsgränsen behåller ai_hangup eller user_hangup |
superseded | Sessionen ersattes av en nyare |
inbound_config_webhook_failed | Inkommande samtal till ett nummer i webhook-läge: konfigurationswebhooken returnerade inte en användbar konfiguration, så samtalet avslutades innan en agent svarade. status är failed och samtalet debiteras inte |
unknown | Orsaken till avslutet kunde inte fastställas |
Transkriptformat
Varje post i transcripts är en samtalstur. Roller är
user (uppringarens tal), model (agentens tal och verktygsanrop),
tool (verktygsresultat) och system (samtalshändelser som språkbyten).
[
{
"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"] }
}
}
]| Fält | Typ | Beskrivning |
|---|---|---|
role | sträng | user, model, tool eller system |
content_type | sträng | text/plain för tal; application/json för verktygsanrop, verktygsresultat och systemhändelser |
content | sträng | objekt | Taltext eller det strukturerade objektet som visas ovan. Verktygsanrop: {"tool_call": name, "arguments": {…}}. Verktygsresultat: {"tool_name": name, "response": {…}} |
start_ms, end_ms | heltal | Förskjutningar från samtalsstart, i ms. Finns när ljudtidtagning är känd |
ttfa_ms | heltal | Tid till första ljudet för en model-tur, när den mäts |
audio_url, audio_urls | sträng / matris | Tidsbegränsade signerade URL:er för turens ljud, när ljud spelas in per tur |
Använd
GET /v1/calls/{call_id}/history
för den fullständigt strukturerade turhistoriken (med avbrottsmarkörer,
bekräftelseuppmaningar och råa positioner).
Skillnader i äldre payload
Det äldre webhook-omslaget med en enda URL är
{"type": "telephony.complete" | "web.complete", "data": {…}} utan
något event_id, och dess data skiljer sig från endpoint-payloaden:
Den äldre slutförandepayloaden innehåller även agent_id och agent_name.
- Turmatrisen finns under
history, intetranscripts(samma turschema som ovan). - Fältuppsättningen är den råa rapporten vid samtalets slut och kan innehålla ytterligare interna fält utöver tabellen ovan — behandla okända fält som information.
- Webbsamtal (
direction: "web") utelämnarfrom_number/to_numberoch lägger tillorigin_domain. - Mikrofonsamtal för test i Builder rapporteras som
telephony.completepå den äldre sökvägen (endpoint-systemet mappar dem tillweb.complete). - Överföringssamordning: när ett samtal avslutas med en överföring anropas
den äldre webhooken synkront och kan svara
{"transfer_ready": false}för att signalera att överföringens mål inte är redo. Alla andra svar (eller ingen äldre webhook) låter överföringen fortsätta. Endpoint-leveranser konsulteras aldrig för detta.
Exempelhanterare
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 });
},
);Vanliga användningsfall
Spara varje samtals transkript och inspelnings-URL tillsammans med dina kundregister.
Strömma transkript till en pipeline för ämnesmodellering, extrahering av CSAT-signaler eller övervakning av överföringsfrekvens.
Öppna samtal i ett QA-verktyg för manuell granskning, eller kör dem genom din egen utvärderingsmodell.
Avisera en mänsklig teammedlem vid överföring eller fel.