telephony.complete / web.complete
Ikke-blokkerende webhook som leveres når en samtale avsluttes, med transkripsjon, URL til opptak og måledata.
En fullføringshendelse utløses etter at hvert anrop avsluttes — innkommende telefoni, utgående telefoni, nettsamtale eller testanrop (mikrofonøkt i byggeren). Den er ikke-blokkerende: svar med en hvilken som helst 2xx-status.
Hendelsen leveres på begge måter:
- Webhook-endepunkter mottar
telephony.complete(telefonanrop) ellerweb.complete(nettsamtaler og testanrop med mikrofon i byggeren) med den stabile nyttelasten som er dokumentert nedenfor, enevent_idper levering, tidsavbrudd på 30 s og nye forsøk i opptil 24 t. - Den eldre webhooken med én URL mottar ett synkront forsøk (tidsavbrudd på 10 s, ingen nye forsøk) med en litt annerledes nyttelast — se Forskjeller i eldre nyttelast.
Forespørselsnyttelast (leveringer til endepunkter)
{
"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å tvers av alle hendelser for dette anropet |
direction | string | inbound, outbound, web, test. Historiske nyttelaster kan inneholde de eldre verdiene mic eller widget |
from_number, to_number | string | E.164. from_number er den bokstavelige verdien "web" for nettsamtaler og testanrop |
origin_domain | string | Kun web/test — opprinnelsen til siden som var vert for widgeten (tom for mikrofonøkter) |
start_time, end_time | timestamp | ISO 8601 UTC |
duration_seconds | integer | null | Utledet fra start/slutt |
status | string | completed eller failed |
end_reason | string | Se tabellen nedenfor |
product, voice | string | Agentkonfigurasjonen som var aktiv på tidspunktet for anropet |
transfer_number | string | null | Angis når anropet ble overført |
recording_url | string | null | Signert URL med utløpstid; last ned raskt. null når ingen opptaksartefakt er tilgjengelig |
billable_minutes | number | Fakturerte minutter, avrundet til nærmeste kvartminutt (15-sekunders intervaller, minimum 0.25). Anrop som går rett til talepost, rapporterer fortsatt de faktiske målte minuttene her, men kostnaden er begrenset til ett minutt til plansatsen. |
billing_total_cents | integer | USD-cent |
transcripts | array | Transkripsjonsoppføringer per samtaletur; kan være tom når en transkripsjon ikke er tilgjengelig |
Avslutningsårsaker
| Verdi | Betydning |
|---|---|
user_hangup | Den eksterne parten la på først |
ai_hangup | Stemmeagenten avsluttet samtalen bevisst |
ai_transfer | Stemmeagenten overførte samtalen; transfer_number er angitt |
ai_warm_transfer | Stemmeagenten fullførte en varm (assistert) overføring |
voicemail_hangup | Talepost ble oppdaget, og samtalen ble avsluttet i henhold til voicemail_action |
max_duration | Anropet nådde grensen for maksimal varighet |
superseded | Økten ble erstattet av en nyere økt |
unknown | Avslutningsårsaken kunne ikke fastslås |
Transkriptformat
Hvert element i transcripts er én samtaletur. 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 lydtidspunkt er kjent |
ttfa_ms | heltall | Tid til første lyd for en model-tur, når målt |
audio_url, audio_urls | streng / matrise | Utløpende signerte URL-er for turens lyd, når lyd er tatt opp per tur |
Bruk den fullstendig strukturerte turhistorikken (med avbruddsmarkører,
bekreftelsesforespørsler og rå posisjoner) via
GET /v1/calls/{call_id}/history.
Forskjeller i eldre payload
Den eldre webhook-konvolutten med én URL er
{"type": "telephony.complete" | "web.complete", "data": {…}} uten
event_id, og data-feltet er forskjellig fra endepunktets payload:
- Turmatrisen ligger under
history, ikketranscripts(samme turskjema som ovenfor). - Feltsettet er den rå rapporten ved slutten av anropet og kan inneholde flere interne felt enn tabellen ovenfor — behandle ukjente felt som informasjon.
- Nettanrop (
direction: "web") utelaterfrom_number/to_numberog legger tilorigin_domain. - Builder-mikrofontestanrop rapporteres som
telephony.completepå den eldre stien (endepunktsystemet tilordner dem tilweb.complete). - Overføringskoordinering: Når et anrop avsluttes med en overføring, blir
den eldre webhooken kalt synkront og kan svare
{"transfer_ready": false}for å signalisere at målet for overføringen ikke er klart. Alle andre svar (eller ingen eldre webhook) lar overføringen fortsette. Endepunktleveringer brukes aldri til dette.
Eksempel på hendelsesbehandler
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 transkripsjonen og opptaks-URL-en for hver samtale sammen med kundepostene dine.
Strøm transkripsjoner 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 / feil.