Open in
telephony.complete / web.complete
Ikke-blokkerende webhook levert når en samtale avsluttes, med transkripsjon, URL til opptak og målinger.
En fullføringshendelse utløses etter at hver samtale avsluttes — innkommende telefoni, utgående telefoni, nettsamtale eller testsamtale (mikrofonøkt i byggeren). Den er ikke-blokkerende: svar med en hvilken som helst 2xx-status.
Hendelsen leveres via begge banene:
- Webhook-endepunkter mottar
telephony.complete(telefonsamtaler) ellerweb.complete(nettsamtaler og mikrofontestsamtaler i byggeren) med den stabile nyttelasten dokumentert nedenfor, enevent_idper levering, en tidsgrense på 30 s og nye forsøk i opptil 24 t. - Den eldre webhooken med én URL mottar ett synkront forsøk (tidsgrense på 10 s, ingen nye forsøk) med en litt annerledes nyttelast — se Forskjeller i eldre nyttelast.
Forespørselsnyttelast (endepunktleveringer)
{
"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 | heltall | Stabil på tvers av alle hendelser for denne samtalen |
agent_id | heltall | null | Stemmeagenten som håndterte samtalen, når en var tilordnet |
agent_name | streng | null | Stemmeagenten som håndterte samtalen, når en var tilordnet |
direction | streng | inbound, outbound, web, test. Historiske nyttelaster kan inneholde eldre mic- eller widget-verdier |
from_number, to_number | streng | E.164. from_number er den bokstavelige verdien "web" for nettsamtaler og testsamtaler |
origin_domain | streng | Kun web/test — sideopprinnelsen som var vert for widgeten (tom for mic-økter) |
start_time, end_time | tidsstempel | ISO 8601 UTC |
duration_seconds | heltall | null | Avledet fra start/slutt |
status | streng | completed eller failed |
end_reason | streng | Se tabellen nedenfor |
product, voice | streng | Stemmeagentkonfigurasjonen som var aktiv på samtaletidspunktet |
variables | objekt | Inndatavariabler med øyeblikksbilde fra da samtalen startet |
unresolved_variables | matrise | Variabelnavn som refereres av samtalekonfigurasjonen, men som ikke ble oppgitt ved samtalestart |
transfer_number | streng | null | Angis når samtalen ble overført |
recording_url | streng | null | Utløpende signert URL; last ned raskt. null når ingen opptaksartefakt er tilgjengelig |
recording_stopped_at | tidsstempel | null | Når opptaket ble stoppet før samtalen var over; ingenting etter dette tidspunktet er med i opptaket. null når opptaket fortsatte til slutten av samtalen, eller samtalen ikke ble tatt opp |
recording_stopped_reason | streng | null | Hvorfor opptaket stoppet tidlig. warm_transfer: det stopper rett før en varm overføring ringer destinasjonen, og forblir avslått selv om overføringen mislykkes og innringeren kommer tilbake til stemmeagenten. Behandle andre verdier som informasjon; flere kan bli lagt til. null når recording_stopped_at er null |
billable_minutes | tall | Fakturerte minutter, avrundet til nærmeste kvart minutt (intervaller på 15 sekunder, minimum 0.25). Samtaler som går rett til talepost rapporterer fortsatt sine faktiske målte minutter her, men kostnaden er begrenset til ett minutt til planprisen. |
billing_total_cents | heltall | USD-cent |
transcripts | matrise | Transkripsjonsoppføringer per tur; kan være tom når en transkripsjon ikke er tilgjengelig |
summary | streng | null | Sammendrag etter samtalen i klart språk (to til fire setninger med løpende tekst, maksimalt 2 000 tegn, skrevet på samtalens språk: stemmeagentens primærspråk med mindre samtalen overstyrte det) når stemmeagenten har Generer et samtalesammendrag aktivert under Data som skal samles inn i stemmeagentbyggeren (call_summary_enabled på Agent) og genereringen lykkes; ellers null, inkludert for samtaler som er for korte til å oppsummere, samtaler uten transkripsjon og stemmeagenter med opptak deaktivert. Sammendraget genereres i samme steg etter samtalen som uthenting, så innstillingen Vent på uthentede data før webhooken for fullført samtale sendes (extraction_blocks_completion) avgjør om denne hendelsen venter på det: I en ventende modus er sammendraget klart før denne hendelsen sendes (selve sammendraget får maksimalt 30 sekunder innenfor stegets budsjett på 75 sekunder); når hendelsen ikke venter (never, eller en overføring under except_transfers), er dette feltet null, og sammendraget sendes ikke på nytt i call.data_extracted, så les det senere fra summary på Call-objektet. Også tilgjengelig på samtalesiden i dashbordet |
extracted_data | objekt | null | Strukturert uthentingsresultat med status, fields, evidence, verification, field_reasons og schema_version. Hvert felt som ikke er null, har det nøyaktige strukturelt kontrollerte sitatet (maksimalt 1 000 tegn), samt talerrollen og turindeksen; lengre sitater returnert av modellen avvises i stedet for å avkortes. Bevis er null når feltet er null. verified betyr at hver kandidat mottok nøyaktig én gyldig, uavhengig vurdering. unavailable dekker også feilformatert eller delvis verifikatorutdata; gyldige delvise vurderinger gjelder fortsatt, mens kandidater uten én gyldig vurdering settes til null. status er completed, failed, exhausted, skipped eller skipped_recording_disabled; null når stemmeagenten ikke hadde noen uthentingsfelt |
steps_trace | matrise | Bare for samtaler der stemmeagenten har steg eller overleveringer. Rekkefølgen samtalen beveget seg gjennom dem i: Hver oppføring har t_ms (millisekunder siden samtalen startet), agent (stemmeagentens nøkkel, a pluss ID-en), step (steg-ID-en, eller null ved en overlevering til en stemmeagent uten steg) og via (start, model, condition, on_enter eller handoff). Inneholder bare ID-er og tidspunkter, så den beholdes selv når opptak er deaktivert |
collected_values | objekt | Bare for samtaler der stemmeagenten har steg eller overleveringer. Verdiene stemmeagenten samlet inn i stegene sine, nøkkelbasert på navnet på innsamlingsfeltet, samt <tool>_result for hvert stegs on_enter_tool-resultat. Utelates når samtalens innhold ikke beholdes (opptak deaktivert) |
Sluttårsaker
| Verdi | Betydning |
|---|---|
user_hangup | Den eksterne parten la på først |
ai_hangup | AI-en avsluttet samtalen med hensikt |
ai_transfer | AI-en videresendte samtalen; transfer_number er angitt |
ai_warm_transfer | AI-en fullførte en varm (betjent) viderekobling |
voicemail_hangup | Telefonsvarer ble oppdaget, og samtalen ble avsluttet i henhold til voicemail_action |
max_duration | Samtalen nådde maksimal varighetsgrense: agentens eller samtalens egen grense, eller plattformgrensen på 6 timer. En samtale som AI-en eller innringeren avslutter etter advarselen om tidsgrensen, beholder ai_hangup eller user_hangup |
superseded | Økten ble erstattet av en nyere økt |
inbound_config_webhook_failed | Innkommende samtale til et nummer i webhook-modus: konfigurasjonswebhooken returnerte ikke en brukbar konfigurasjon, så samtalen ble avsluttet før en agent svarte. status er failed, og samtalen faktureres ikke |
unknown | Avslutningsårsaken kunne ikke fastslås |
Transkriptformat
Hver oppføring i transcripts er én samtaleomgang. Rollene er
user (innringerens tale), model (agentens tale og verktøykall),
tool (verktøyresultater) og system (anropshendelser, som
språkbytter).
[
{
"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 verktøykall, verktøyresultater og systemhendelser |
content | streng | objekt | Taletekst eller det strukturerte objektet vist ovenfor. Verktøykall: {"tool_call": name, "arguments": {…}}. Verktøyresultater: {"tool_name": name, "response": {…}} |
start_ms, end_ms | heltall | Forskyvninger fra anropsstart, i ms. Finnes når lydtiming er kjent |
ttfa_ms | heltall | Tid til første lyd for en model-omgang, når målt |
audio_url, audio_urls | streng / matrise | Utløpende signerte URL-er for omgangens lyd, når lyd tas opp per omgang |
For den fullstendig strukturerte omgangshistorikken (med avbruddsmarkører,
bekreftelsesforespørsler og rå posisjoner), bruk
GET /v1/calls/{call_id}/history.
Forskjeller i eldre nyttelast
Den eldre webhook-innpakningen med én URL er
{"type": "telephony.complete" | "web.complete", "data": {…}} uten
event_id, og data-feltet skiller seg fra endepunktets nyttelast:
Den eldre fullføringsnyttelasten inneholder også agent_id og agent_name.
- Omgangslisten ligger under
history, ikketranscripts(samme omgangsskjema som ovenfor). - Feltsettet er den rå rapporten ved anropsslutt og kan inneholde ekstra interne felt utover tabellen ovenfor — behandle ukjente felt som informasjon.
- Nettanrop (
direction: "web") utelaterfrom_number/to_numberog legger tilorigin_domain. - Byggerens mikrofontestanrop rapporteres som
telephony.completepå den eldre banen (endepunktssystemet tilordner dem tilweb.complete). - Koordinering av overføring: Når et anrop avsluttes med en overføring,
kalles den eldre webhooken synkront og kan svare
{"transfer_ready": false}for å signalisere at målet for overføringen ikke er klart. Ethvert annet svar (eller ingen eldre webhook) lar overføringen fortsette. Leveringer til endepunktet blir aldri konsultert for dette.
Eksempelbehandler
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 });
},
);Vanlige bruksområder
Lagre hvert samtaletranskript og hver opptaks-URL sammen med kundepostene dine.
Send transkripter til en pipeline for emnemodellering, uttrekking av CSAT-signaler eller overvåking av overføringsrate.
Åpne samtaler i et QA-verktøy for menneskelig gjennomgang, eller kjør dem gjennom din egen evalueringsmodell.
Varsle en menneskelig kollega ved overføring eller feil.