telephony.complete / web.complete
Icke-blockerande webhook som levereras när ett samtal avslutas, med transkript, 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 (mikrofonsession i byggaren). 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 mikrofonsamtal för test i byggaren) med den stabila nyttolast som dokumenteras nedan, ettevent_idper leverans, en tidsgräns på 30 s och omförsök i upp till 24 h. - Den äldre enkel-URL-webhooken tar emot ett synkront försök (tidsgräns på 10 s, inga omförsök) med en något annorlunda nyttolast — se Skillnader i äldre nyttolast.
Begäransnyttolast (slutpunktsleveranser)
{
"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"
}| Fält | Typ | Beskrivning |
|---|---|---|
call_id | heltal | Stabilt för varje händelse för detta samtal |
direction | sträng | inbound, outbound, web, test. Historiska nyttolaster kan innehålla äldre värdena mic eller widget |
from_number, to_number | sträng | E.164. from_number är bokstavligen "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 | Agentkonfigurationen som gällde vid samtalstillfället |
transfer_number | sträng | null | Anges när samtalet har kopplats vidare |
recording_url | sträng | null | Tidsbegränsad signerad URL; ladda ned omgående. null när ingen inspelning är tillgänglig |
billable_minutes | tal | Debiterade minuter, avrundade till närmaste kvartsminut (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 |
Avslutsorsaker
| Värde | Betydelse |
|---|---|
user_hangup | Motparten lade på först |
ai_hangup | Röstagenten avslutade samtalet medvetet |
ai_transfer | Röstagenten kopplade vidare samtalet; transfer_number anges |
ai_warm_transfer | Röstagenten slutförde en varm (övervakad) vidarekoppling |
voicemail_hangup | Röstbrevlådan identifierades och samtalet avslutades enligt din voicemail_action |
max_duration | Samtalet nådde den maximala tidsgränsen |
superseded | Sessionen ersattes av en nyare |
unknown | Avslutsorsaken 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 ljudtiming ä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 det spelas in per tur |
Använd
GET /v1/calls/{call_id}/history
för den fullständigt strukturerade turhistoriken (med avbrottsmarkörer,
bekräftelsefrågor och råa positioner).
Skillnader i äldre payloads
Det äldre webhook-kuvertet med en enda URL är
{"type": "telephony.complete" | "web.complete", "data": {…}} utan
event_id, och dess data skiljer sig från endpoint-payloaden:
- Turmatrisen finns under
history, intetranscripts(samma tur-schema 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 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åladress 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 transkription och inspelnings-URL tillsammans med dina kundregister.
Strömma transkriptioner till en pipeline för ämnesmodellering, extrahering av CSAT-signaler eller övervakning av överföringsfrekvens.
Öppna samtal i ett QA-verktyg för mänsklig granskning, eller kör dem genom din egen utvärderingsmodell.
Avisera en mänsklig teammedlem vid överföring eller fel.