telephony.complete / web.complete
Neblokující webhook doručený po ukončení hovoru s přepisem, URL záznamu a metrikami.
Událost dokončení se vyvolá po skončení každého hovoru — příchozího telefonního hovoru, odchozího telefonního hovoru, webového hovoru nebo testovacího hovoru (relace mikrofonu v editoru). Je neblokující: odpovězte libovolným kódem 2xx.
Událost se doručuje 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, identifikátoremevent_idpro každé doručení, 30sekundovým časovým limitem a opakováním až po dobu 24 h. - Starší webhook s jedinou adresou URL přijímá jeden synchronní pokus (10sekundový časový limit, bez opakování) s mírně odlišným datovým obsahem — viz Rozdíly ve starším datovém obsahu.
Datový obsah požadavku (doručení na koncový bod)
{
"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"
}| Pole | Typ | Popis |
|---|---|---|
call_id | integer | Neměnné ve všech událostech pro tento hovor |
direction | string | inbound, outbound, web, test. Historické datové obsahy mohou obsahovat starší hodnoty mic nebo widget |
from_number, to_number | string | E.164. Pro webové a testovací hovory je from_number doslovně "web" |
origin_domain | string | Pouze web/test — původ stránky, na které byl widget hostován (pro relace mikrofonu prázdné) |
start_time, end_time | timestamp | ISO 8601 UTC |
duration_seconds | integer | null | Odvozeno z času začátku a konce |
status | string | completed nebo failed |
end_reason | string | Viz tabulka níže |
product, voice | string | Konfigurace agenta platná v době hovoru |
transfer_number | string | null | Nastaví se při přepojení hovoru |
recording_url | string | null | Podepsaná adresa URL s omezenou platností; stáhněte ji neprodleně. null, pokud není k dispozici záznam hovoru |
billable_minutes | number | Účtované minuty zaokrouhlené na nejbližší čtvrt minuty (15sekundové intervaly, 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 | integer | Centy USD |
transcripts | array | Položky přepisu pro jednotlivé repliky; při nedostupném přepisu může být prázdné |
Důvody ukončení
| Hodnota | Význam |
|---|---|
user_hangup | Protistrana zavěsila jako první |
ai_hangup | Agent AI hovor záměrně ukončil |
ai_transfer | Agent AI hovor přepojil; je nastaveno transfer_number |
ai_warm_transfer | Agent AI dokončil teplé (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 limitu maximální délky |
superseded | Relace byla nahrazena novější relací |
unknown | Důvod ukončení nebylo možné určit |
Formát přepisu
Každá položka v transcripts představuje jeden konverzační tah. 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 změny
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 přítomné, když je známé časování zvuku |
ttfa_ms | celé číslo | Doba do prvního zvuku pro tah model, pokud je měřena |
audio_url, audio_urls | řetězec / pole | URL s dočasným podpisem pro zvuk tahu, pokud je zvuk zaznamenáván 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 staršího payloadu
Starší webhookový obal s jedinou adresou URL je
{"type": "telephony.complete" | "web.complete", "data": {…}} bez
event_id a jeho data se liší od payloadu koncového bodu:
- Pole tahů je v
history, nikoli vtranscripts(stejné schéma tahů jako výše). - Sada polí představuje nezpracovaný report o ukončení hovoru a může obsahovat další interní pole nad rámec výše uvedené tabulky — s neznámými poli zacházejte jako s informativními.
- Webové hovory (
direction: "web") vynechávajífrom_number/to_numbera přidávajíorigin_domain. - Testovací hovory mikrofonu v Builderu se ve starší cestě hlásí jako
telephony.complete(systém koncového bodu je mapuje naweb.complete). - Koordinace přepojení: když hovor skončí přepojením, starší webhook se
volá synchronně a může vrátit
{"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í koncovému bodu se k tomuto účelu nikdy nekonzultuje.
Příklad obslužné funkce
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 spolu se záznamy o zákaznících.
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 lidskou kontrolu nebo je spouštějte prostřednictvím vlastního vyhodnocovacího modelu.
Při přepojení nebo selhání upozorněte lidského člena týmu.