Open in
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 consegnato su entrambi i percorsi:
- Gli endpoint webhook ricevono
telephony.complete(chiamate telefoniche) oweb.complete(chiamate web e chiamate di test con microfono del builder) con il payload stabile documentato di seguito, unevent_idper ogni consegna, un timeout di 30 s e nuovi tentativi per un massimo di 24 h. - Il webhook legacy a URL singolo riceve un singolo tentativo sincrono (timeout di 10 s, nessun nuovo tentativo) con un payload leggermente diverso — consulta Differenze del payload legacy.
Payload della richiesta (consegne 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",
"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"
}| Campo | Tipo | Descrizione |
|---|---|---|
call_id | integer | Stabile in ogni evento relativo a questa chiamata |
agent_id | integer | null | Agente che ha gestito la chiamata, quando assegnato |
agent_name | string | null | Agente che ha gestito la chiamata, quando assegnato |
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 mic) |
start_time, end_time | timestamp | ISO 8601 UTC |
duration_seconds | integer | null | Derivato dall'ora di inizio/fine |
status | string | completed o failed |
end_reason | string | Vedi la tabella seguente |
product, voice | string | Configurazione dell'agente in vigore al momento della chiamata |
variables | object | Variabili di input salvate all'avvio della chiamata |
unresolved_variables | array | Nomi delle variabili a cui fa riferimento la configurazione della chiamata ma non fornite all'avvio 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 artefatto di registrazione |
recording_stopped_at | timestamp | null | Quando la registrazione è stata interrotta prima della fine della chiamata; nella registrazione non è presente nulla dopo questo momento. null quando la registrazione è proseguita fino alla fine della chiamata o la chiamata non è stata registrata |
recording_stopped_reason | string | null | Perché la registrazione è stata interrotta anticipatamente. warm_transfer: si interrompe immediatamente prima che un trasferimento assistito chiami la destinazione e rimane disattivata anche se il trasferimento fallisce e il chiamante torna all'agente. Considera gli altri valori come informativi; potrebbero esserne aggiunti altri. null quando recording_stopped_at è null |
billable_minutes | number | Minuti fatturati, arrotondati al quarto di minuto più vicino (incrementi di 15 secondi, minimo 0,25). Le chiamate dirette alla segreteria telefonica 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 |
summary | string | null | Riepilogo post-chiamata in linguaggio semplice (da due a quattro frasi di testo, al massimo 2.000 caratteri, scritto nella lingua della chiamata: la lingua principale dell'agente, salvo diversa impostazione durante la chiamata) quando l'agente ha attivato Genera un riepilogo della chiamata in Dati da raccogliere nel builder dell'agente (call_summary_enabled nell'Agente) e la generazione è riuscita; null negli altri casi, incluse le chiamate troppo brevi da riepilogare, le chiamate senza trascrizione e gli agenti con la registrazione disattivata. Il riepilogo viene generato nello stesso passaggio post-chiamata dell'estrazione, quindi l'impostazione Attendi i dati estratti prima di inviare il webhook di fine chiamata (extraction_blocks_completion) determina se questo evento lo attende: in una modalità di attesa il riepilogo è pronto prima dell'invio di questo evento (il riepilogo stesso riceve al massimo 30 secondi all'interno del budget di 75 secondi del passaggio); quando l'evento non attende (never o un trasferimento con except_transfers), questo campo è null e il riepilogo non viene inviato nuovamente in call.data_extracted, quindi leggilo in seguito da summary nell'oggetto Call. Disponibile anche nella pagina della chiamata nella dashboard |
extracted_data | object | null | Risultato dell'estrazione strutturata con status, fields, evidence, verification, field_reasons e schema_version. Ogni campo non nullo contiene la citazione esatta verificata strutturalmente (al massimo 1.000 caratteri), oltre al ruolo dell'interlocutore e all'indice del turno; le citazioni più lunghe restituite dal modello vengono rifiutate anziché troncate. L'evidenza è null quando il campo è null. verified indica che ogni candidato ha ricevuto esattamente un verdetto indipendente valido. unavailable copre anche output del verificatore non validi o parziali; i verdetti parziali validi vengono comunque applicati, mentre i candidati senza un verdetto valido vengono impostati su null. status è completed, failed, exhausted, skipped o skipped_recording_disabled; null quando l'agente non aveva campi di estrazione |
steps_trace | array | Solo per le chiamate il cui agente ha passaggi o handoff. L'ordine in cui la chiamata li ha attraversati: ogni voce contiene t_ms (millisecondi dall'avvio della chiamata), agent (la chiave dell'agente, a più il relativo ID), step (l'ID del passaggio, oppure null in caso di handoff a un agente senza passaggi) e via (start, model, condition, on_enter o handoff). Contiene solo ID e orari, quindi viene mantenuto anche quando la registrazione è disattivata |
collected_values | object | Solo per le chiamate il cui agente ha passaggi o handoff. I valori raccolti dall'agente nei suoi passaggi, indicizzati per nome del campo di raccolta, più <tool>_result per il risultato on_enter_tool di ciascun passaggio. Ometto quando il contenuto della chiamata non viene conservato (registrazione disattivata) |
Motivi di conclusione
| Valore | Significato |
|---|---|
user_hangup | La controparte ha riagganciato per prima |
ai_hangup | L'AI ha terminato deliberatamente la chiamata |
ai_transfer | L'AI ha trasferito la chiamata; transfer_number è impostato |
ai_warm_transfer | L'AI 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: quello dell'agente, quello specifico della chiamata o il limite della piattaforma di 6 ore. Una chiamata terminata dall'AI o dal chiamante dopo l'avviso del limite di tempo mantiene ai_hangup o user_hangup |
superseded | La sessione è stata sostituita da una più recente |
inbound_config_webhook_failed | Chiamata in entrata a un numero in modalità webhook: il webhook di configurazione non ha restituito una configurazione utilizzabile, quindi la chiamata è stata terminata prima che un agente rispondesse. status è failed e la chiamata non viene fatturata |
unknown | Non è stato possibile determinare il motivo della terminazione |
Formato della trascrizione
Ogni voce in transcripts corrisponde a un turno conversazionale. I ruoli sono
user (voce del chiamante), model (voce dell'agente e chiamate di strumenti),
tool (risultati degli strumenti) e system (eventi della chiamata, ad esempio 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 chiamate di strumenti, risultati degli strumenti ed eventi di sistema |
content | string | object | Testo vocale oppure l'oggetto strutturato mostrato sopra. Chiamate di 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 la temporizzazione audio è nota |
ttfa_ms | integer | Tempo fino al primo audio per un turno model, quando misurato |
audio_url, audio_urls | string / array | URL firmati in scadenza per l'audio del turno, quando registrato per turno |
Per la cronologia dei turni completamente strutturata (con indicatori di interruzione,
prompt di conferma 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 i relativi data differiscono dal payload dell'endpoint:
Il payload di completamento legacy include inoltre agent_id e agent_name.
- L'array dei turni si trova in
history, non intranscripts(stesso schema dei turni mostrato sopra). - L'insieme di campi è il report grezzo di fine chiamata e può includere campi interni aggiuntivi oltre a quelli nella tabella precedente: considera i campi sconosciuti come informativi.
- Le chiamate web (
direction: "web") omettonofrom_number/to_numbere aggiungonoorigin_domain. - Le chiamate di test del microfono nel Builder vengono segnalate come
telephony.completenel percorso legacy (il sistema dell'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 webhook legacy) consente al trasferimento di procedere. Le consegne all'endpoint non vengono mai consultate per questo.
Esempio di handler
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 dati dei clienti.
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 elaborale con il tuo modello di valutazione.
Avvisa un membro del team umano in caso di trasferimento o errore.