telephony.complete / web.complete
Webhook non bloccante inviato al termine di una chiamata, con trascrizione, URL della registrazione e metriche.
Un evento di completamento viene attivato al termine di ogni chiamata: telefonia in entrata, telefonia in uscita, chiamata web o chiamata di test (sessione microfono del builder). È non bloccante: rispondi con qualsiasi codice 2xx.
L'evento viene recapitato tramite entrambi i percorsi:
- Gli endpoint webhook ricevono
telephony.complete(chiamate telefoniche) oweb.complete(chiamate web e chiamate di test del microfono nel builder) con il payload stabile documentato di seguito, unevent_idper ogni recapito, un timeout di 30 s e tentativi fino a 24 h. - Il webhook legacy a URL singolo riceve un tentativo sincrono (timeout di 10 s, nessun tentativo aggiuntivo) con un payload leggermente diverso: consulta Differenze del payload legacy.
Payload della richiesta (recapiti agli endpoint)
{
"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"
}| Campo | Tipo | Descrizione |
|---|---|---|
call_id | integer | Stabile in tutti gli eventi di questa chiamata |
direction | string | inbound, outbound, web, test. I payload storici possono contenere i valori legacy mic o widget |
from_number, to_number | string | E.164. from_number è il valore letterale "web" per le chiamate web e di test |
origin_domain | string | Solo web/test — l'origine della pagina che ospitava il widget (vuoto per le sessioni microfono) |
start_time, end_time | timestamp | ISO 8601 UTC |
duration_seconds | integer | null | Derivato dall'orario di inizio/fine |
status | string | completed o failed |
end_reason | string | Consulta la tabella seguente |
product, voice | string | Configurazione dell'agente in uso al momento della chiamata |
transfer_number | string | null | Impostato quando la chiamata è stata trasferita |
recording_url | string | null | URL firmato con scadenza; scaricalo tempestivamente. null quando non è disponibile alcun file di registrazione |
billable_minutes | number | Minuti fatturati, arrotondati al quarto di minuto più vicino (incrementi di 15 secondi, minimo 0,25). Le chiamate inoltrate direttamente alla segreteria riportano comunque qui i minuti effettivamente misurati, ma l'addebito è limitato a un minuto alla tariffa del piano. |
billing_total_cents | integer | Centesimi USD |
transcripts | array | Voci della trascrizione per turno; può essere vuoto quando una trascrizione non è disponibile |
Motivi di fine
| Valore | Significato |
|---|---|
user_hangup | La parte remota ha riagganciato per prima |
ai_hangup | L'IA ha terminato deliberatamente la chiamata |
ai_transfer | L'IA ha trasferito la chiamata; transfer_number è impostato |
ai_warm_transfer | L'IA ha completato un trasferimento assistito |
voicemail_hangup | È stata rilevata la segreteria telefonica e la chiamata è terminata in base a voicemail_action |
max_duration | La chiamata ha raggiunto il limite massimo di durata |
superseded | La sessione è stata sostituita da una più recente |
unknown | Non è stato possibile determinare il motivo della fine |
Formato della trascrizione
Ogni voce in transcripts corrisponde a un turno della conversazione. I ruoli sono
user (voce del chiamante), model (voce dell'agente e chiamate agli strumenti),
tool (risultati degli strumenti) e system (eventi della chiamata come i cambi
di lingua).
[
{
"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"] }
}
}
]| Campo | Tipo | Descrizione |
|---|---|---|
role | string | user, model, tool o system |
content_type | string | text/plain per la voce; application/json per le chiamate agli strumenti, i risultati degli strumenti e gli eventi di sistema |
content | string | object | Testo vocale oppure l'oggetto strutturato mostrato sopra. Chiamate agli strumenti: {"tool_call": name, "arguments": {…}}. Risultati degli strumenti: {"tool_name": name, "response": {…}} |
start_ms, end_ms | integer | Offset dall'inizio della chiamata, in ms. Presenti quando è noto il timing audio |
ttfa_ms | integer | Tempo al primo audio per un turno model, quando misurato |
audio_url, audio_urls | string / array | URL firmati a scadenza per l'audio del turno, quando registrato per turno |
Per la cronologia dei turni completamente strutturata (con indicatori di interruzione,
ack-prompt e posizioni grezze), usa
GET /v1/calls/{call_id}/history.
Differenze del payload legacy
L'envelope webhook legacy a URL singolo è
{"type": "telephony.complete" | "web.complete", "data": {…}} senza
event_id, e il relativo data differisce dal payload dell'endpoint:
- L'array dei turni è in
history, non intranscripts(stesso schema dei turni mostrato sopra). - L'insieme dei campi è il report grezzo di fine chiamata e può includere campi interni aggiuntivi oltre a quelli della tabella sopra: considera i campi sconosciuti come informativi.
- Le chiamate web (
direction: "web") omettonofrom_number/to_numbere aggiungonoorigin_domain. - Le chiamate di test del microfono Builder vengono segnalate come
telephony.completenel percorso legacy (il sistema endpoint le mappa aweb.complete). - Coordinamento del trasferimento: quando una chiamata termina con un trasferimento, il
webhook legacy viene chiamato in modo sincrono e può rispondere
{"transfer_ready": false}per segnalare che la destinazione del passaggio non è pronta. Qualsiasi altra risposta (o l'assenza di un webhook legacy) consente al trasferimento di procedere. Le consegne dell'endpoint non vengono mai consultate per questo.
Gestore di esempio
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 });
},
);Casi d'uso comuni
Salva la trascrizione e l'URL della registrazione di ogni chiamata insieme ai tuoi record cliente.
Invia le trascrizioni a una pipeline per la modellazione degli argomenti, l'estrazione di segnali CSAT o il monitoraggio del tasso di trasferimento.
Apri le chiamate in uno strumento QA per la revisione umana oppure eseguile con il tuo modello di valutazione.
Avvisa un membro del team umano in caso di trasferimento / errore.