Open in
telephony.complete / web.complete
Neblokující webhook doručený po ukončení hovoru s přepisem, URL nahrávky a metrikami.
Událost dokončení se spustí po ukončení každého hovoru — příchozí telefonie, odchozí telefonie, webového hovoru nebo testovacího hovoru (relace mikrofonu v editoru). Je neblokující: odpovězte libovolným stavem 2xx.
Událost je doručena oběma cestami:
- Koncové body webhooků přijímají
telephony.complete(telefonní hovory) neboweb.complete(webové hovory a testovací hovory mikrofonu v editoru) se stabilním datovým obsahem popsaným níže,event_idpro každé doručení, časovým limitem 30 s a opakovanými pokusy po dobu až 24 h. - Starší webhook s jedinou URL přijímá jeden synchronní pokus (časový limit 10 s, bez opakování) s mírně odlišným datovým obsahem — viz Rozdíly ve starším datovém obsahu.
Datová část požadavku (doručení endpointu)
{
"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"
}| Pole | Typ | Popis |
|---|---|---|
call_id | celé číslo | Stabilní napříč všemi událostmi tohoto hovoru |
agent_id | celé číslo | null | Agent, který hovor vyřídil, pokud byl přiřazen |
agent_name | řetězec | null | Agent, který hovor vyřídil, pokud byl přiřazen |
direction | řetězec | inbound, outbound, web, test. Historické datové pakety mohou obsahovat starší hodnoty mic nebo widget |
from_number, to_number | řetězec | E.164. from_number má pro webové a testovací hovory doslovnou hodnotu "web" |
origin_domain | řetězec | Pouze pro web/test — původ stránky, na které byl widget hostován (pro relace mic je prázdný) |
start_time, end_time | časové razítko | ISO 8601 UTC |
duration_seconds | celé číslo | null | Odvozeno z času začátku/konce |
status | řetězec | completed nebo failed |
end_reason | řetězec | Viz tabulku níže |
product, voice | řetězec | Konfigurace agenta platná v době hovoru |
variables | objekt | Vstupní proměnné zachycené při zahájení hovoru |
unresolved_variables | pole | Názvy proměnných, na které odkazuje konfigurace hovoru, ale které nebyly dodány při zahájení hovoru |
transfer_number | řetězec | null | Nastaveno při přepojení hovoru |
recording_url | řetězec | null | Podepsaná adresa URL s omezenou platností; stáhněte ji neprodleně. null, když není k dispozici žádný záznam hovoru |
recording_stopped_at | časové razítko | null | Okamžik, kdy bylo nahrávání zastaveno před koncem hovoru; nic po tomto okamžiku v nahrávce není. null, když nahrávání pokračovalo až do konce hovoru nebo hovor nebyl nahráván |
recording_stopped_reason | řetězec | null | Důvod předčasného zastavení nahrávání. warm_transfer: nahrávání se zastaví těsně předtím, než teplé přepojení zavolá cílovou stranu, a zůstane vypnuté i v případě, že přepojení selže a volající se vrátí k agentovi. Ostatní hodnoty považujte za informativní; mohou být přidány další. null, když má recording_stopped_at hodnotu null |
billable_minutes | číslo | Fakturované minuty zaokrouhlené na nejbližší čtvrtminutu (v krocích po 15 sekundách, minimum 0.25). Hovory přímo do hlasové schránky zde stále uvádějí skutečně naměřené minuty, ale poplatek je omezen na jednu minutu podle sazby tarifu. |
billing_total_cents | celé číslo | Centy USD |
transcripts | pole | Položky přepisu pro jednotlivé repliky; může být prázdné, když přepis není k dispozici |
summary | řetězec | null | Shrnutí po hovoru v běžném jazyce (dvě až čtyři věty prózy, maximálně 2 000 znaků, napsané v jazyce hovoru: primárním jazyce agenta, pokud ho hovor nepřepsal), když má agent v nástroji pro tvorbu agenta v části Data k získání zapnutou možnost Generovat shrnutí hovoru (call_summary_enabled u Agenta) a generování proběhlo úspěšně; jinak null, včetně hovorů příliš krátkých na shrnutí, hovorů bez přepisu a agentů s vypnutým nahráváním. Shrnutí se generuje ve stejném kroku po hovoru jako extrakce, takže nastavení Čekat na extrahovaná data před odesláním webhooku o ukončení hovoru (extraction_blocks_completion) určuje, zda na něj tato událost čeká: v režimu čekání je shrnutí připraveno před odesláním této události (samotné shrnutí má v rámci 75sekundového rozpočtu kroku nejvýše 30 sekund); když událost nečeká (never nebo přepojení v režimu except_transfers), má toto pole hodnotu null a shrnutí se znovu neodesílá v call.data_extracted, proto jej později načtěte z summary u objektu hovoru. K dispozici je také na stránce hovoru v dashboardu |
extracted_data | objekt | null | Výsledek strukturované extrakce s status, fields, evidence, verification, field_reasons a schema_version. Každé pole jiné než null obsahuje přesnou strukturálně ověřenou citaci (maximálně 1 000 znaků) spolu s rolí mluvčího a indexem repliky; delší citace vrácené modelem jsou místo zkrácení zamítnuty. Důkaz má hodnotu null, když má pole hodnotu null. verified znamená, že každý kandidát získal přesně jeden platný nezávislý verdikt. unavailable zahrnuje také chybný nebo neúplný výstup ověřovatele; platné částečné verdikty se stále použijí, zatímco kandidáti bez jednoho platného verdiktu získají hodnotu null. status je completed, failed, exhausted, skipped nebo skipped_recording_disabled; null, když agent neměl žádná pole pro extrakci |
steps_trace | pole | Pouze u hovorů, jejichž agent má kroky nebo předání. Pořadí, v jakém jimi hovor procházel: každá položka obsahuje t_ms (milisekundy od začátku hovoru), agent (klíč agenta, a plus jeho ID), step (ID kroku nebo null při předání agentovi bez kroků) a via (start, model, condition, on_enter nebo handoff). Obsahuje pouze ID a časy, proto se uchovává i při vypnutém nahrávání |
collected_values | objekt | Pouze u hovorů, jejichž agent má kroky nebo předání. Hodnoty, které agent získal ve svých krocích, klíčované názvem pole pro získání, a také <tool>_result pro výsledek on_enter_tool každého kroku. Vynecháno, když se obsah hovoru neuchovává (nahrávání je vypnuté) |
Důvody ukončení
| Hodnota | Význam |
|---|---|
user_hangup | Volaná strana zavěsila jako první |
ai_hangup | AI hovor záměrně ukončila |
ai_transfer | AI hovor přepojila; je nastaveno transfer_number |
ai_warm_transfer | AI dokončila asistované přepojení |
voicemail_hangup | Byla rozpoznána hlasová schránka a hovor byl ukončen podle vašeho voicemail_action |
max_duration | Hovor dosáhl maximálního limitu délky: limitu agenta, vlastního limitu hovoru nebo šestihodinového limitu platformy. Hovor, který AI nebo volající ukončí po upozornění na časový limit, si zachová ai_hangup nebo user_hangup |
superseded | Relace byla nahrazena novější |
inbound_config_webhook_failed | Příchozí hovor na číslo v režimu webhooku: konfigurační webhook nevrátil použitelnou konfiguraci, proto byl hovor ukončen předtím, než agent odpověděl. status je failed a hovor není účtován |
unknown | Důvod ukončení se nepodařilo určit |
Formát přepisu
Každá položka v transcripts představuje jeden tah konverzace. Role jsou
user (řeč volajícího), model (řeč agenta a volání nástrojů),
tool (výsledky nástrojů) a system (události hovoru, například přepnutí
jazyka).
[
{
"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"] }
}
}
]| Pole | Typ | Popis |
|---|---|---|
role | řetězec | user, model, tool nebo system |
content_type | řetězec | text/plain pro řeč; application/json pro volání nástrojů, výsledky nástrojů a systémové události |
content | řetězec | objekt | Text řeči nebo strukturovaný objekt uvedený výše. Volání nástrojů: {"tool_call": name, "arguments": {…}}. Výsledky nástrojů: {"tool_name": name, "response": {…}} |
start_ms, end_ms | celé číslo | Posuny od začátku hovoru v ms. Jsou uvedeny, když je známé časování audia |
ttfa_ms | celé číslo | Doba do prvního audia pro tah model, pokud je měřena |
audio_url, audio_urls | řetězec / pole | URL s podpisem a omezenou platností pro audio daného tahu, pokud se audio nahrává pro jednotlivé tahy |
Pro plně strukturovanou historii tahů (se značkami přerušení,
výzvami k potvrzení a nezpracovanými pozicemi) použijte
GET /v1/calls/{call_id}/history.
Rozdíly ve starším datovém formátu
Starší obálka webhooku s jednou URL je
{"type": "telephony.complete" | "web.complete", "data": {…}} bez
event_id a její data se liší od datového formátu endpointu:
Starší datový formát dokončení rovněž obsahuje agent_id a agent_name.
- Pole tahů je v
history, nikoli vtranscripts(stejné schéma tahů jako výše). - Sada polí představuje nezpracovaný report po ukončení hovoru a může zahrnovat další interní pole nad rámec výše uvedené tabulky — neznámá pole považujte za informativní.
- Webové hovory (
direction: "web") vynechávajífrom_number/to_numbera přidávajíorigin_domain. - Testovací hovory mikrofonu v nástroji Builder se ve starší cestě hlásí jako
telephony.complete(systém endpointu je mapuje naweb.complete). - Koordinace přepojení: když hovor končí přepojením, starší webhook je
volán synchronně a může odpovědět
{"transfer_ready": false}, aby signalizoval, že cíl přepojení není připraven. Jakákoli jiná odpověď (nebo žádný starší webhook) umožní pokračování přepojení. Doručení endpointu se pro tento účel nikdy nekontroluje.
Příklad obslužné rutiny
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 });
},
);Běžné případy použití
Ukládejte přepis a adresu URL nahrávky každého hovoru společně se záznamy zákazníků.
Streamujte přepisy do pipeline pro modelování témat, extrakci signálů CSAT nebo sledování míry přepojení.
Otevírejte hovory v nástroji QA pro kontrolu člověkem nebo je nechte vyhodnotit vlastním hodnoticím modelem.
Při přepojení nebo selhání upozorněte člena týmu.