telephony.complete / web.complete
Udalosť dokončenia sa spustí po skončení každého hovoru — prichádzajúcej telefónie, odchádzajúcej telefónie, webového hovoru alebo testovacieho hovoru (relácia mikrofónu v nástroji na tvorbu). Je neblokujúca: odpovedzte ľubovoľným kódom 2xx.
Udalosť sa doručuje oboma spôsobmi:
- Koncové body webhookov prijímajú
telephony.complete(telefónne hovory) aleboweb.complete(webové hovory a testovacie hovory z mikrofónu v nástroji na tvorbu) so stabilným dátovým objektom zdokumentovaným nižšie, identifikátoromevent_idpre každé doručenie, časovým limitom 30 s a opakovanými pokusmi až 24 h. - Starší webhook s jednou adresou URL prijíma jeden synchrónny pokus (časový limit 10 s, bez opakovaných pokusov) s mierne odlišným dátovým objektom — pozrite si Rozdiely v staršom dátovom objekte.
Dátový objekt požiadavky (doručenia koncovému bodu)
{
"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 | celé číslo | Stabilné vo všetkých udalostiach pre tento hovor |
direction | reťazec | inbound, outbound, web, test. Historické dátové objekty môžu obsahovať staršie hodnoty mic alebo widget |
from_number, to_number | reťazec | E.164. Pri webových a testovacích hovoroch má from_number doslovnú hodnotu "web" |
origin_domain | reťazec | Len web/test — origin stránky, na ktorej je umiestnený widget (pri reláciách mikrofónu prázdne) |
start_time, end_time | časová značka | ISO 8601 UTC |
duration_seconds | celé číslo | null | Odvodené od času začiatku/konca |
status | reťazec | completed alebo failed |
end_reason | reťazec | Pozrite si tabuľku nižšie |
product, voice | reťazec | Konfigurácia agenta použitá v čase hovoru |
transfer_number | reťazec | null | Nastavené pri prepojení hovoru |
recording_url | reťazec | null | Podpísaná adresa URL s obmedzenou platnosťou; stiahnite ju bezodkladne. null, ak nie je dostupný záznam hovoru |
billable_minutes | číslo | Fakturované minúty zaokrúhlené na najbližšiu štvrtinu minúty (15-sekundové intervaly, minimum 0.25). Hovory presmerované priamo do hlasovej schránky tu stále uvádzajú svoje skutočné namerané minúty, ale poplatok je pri sadzbe plánu obmedzený na jednu minútu. |
billing_total_cents | celé číslo | Centy USD |
transcripts | pole | Položky prepisu jednotlivých ťahov; môže byť prázdne, ak prepis nie je k dispozícii |
Dôvody ukončenia
| Hodnota | Význam |
|---|---|
user_hangup | Vzdialená strana zavesila ako prvá |
ai_hangup | Hlasový agent zámerne ukončil hovor |
ai_transfer | Hlasový agent hovor prepojil; transfer_number je nastavené |
ai_warm_transfer | Hlasový agent dokončil teplé (asistované) prepojenie |
voicemail_hangup | Bola rozpoznaná hlasová schránka a hovor bol ukončený podľa vášho nastavenia voicemail_action |
max_duration | Hovor dosiahol limit maximálneho trvania |
superseded | Relácia bola nahradená novšou |
unknown | Dôvod ukončenia sa nepodarilo určiť |
Formát prepisu
Každá položka v transcripts predstavuje jeden konverzačný ťah. Roly sú
user (reč volajúceho), model (reč agenta a volania nástrojov),
tool (výsledky nástrojov) a system (udalosti hovoru, napríklad zmeny
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 | reťazec | user, model, tool alebo system |
content_type | reťazec | text/plain pre reč; application/json pre volania nástrojov, výsledky nástrojov a systémové udalosti |
content | reťazec | objekt | Text reči alebo štruktúrovaný objekt zobrazený vyššie. Volania nástrojov: {"tool_call": name, "arguments": {…}}. Výsledky nástrojov: {"tool_name": name, "response": {…}} |
start_ms, end_ms | celé číslo | Posuny od začiatku hovoru v ms. Sú prítomné, keď je známe časovanie zvuku |
ttfa_ms | celé číslo | Čas do prvého zvuku pre ťah model, ak sa meria |
audio_url, audio_urls | reťazec / pole | Časovo obmedzené podpísané adresy URL pre zvuk daného ťahu, ak sa zaznamenáva po jednotlivých ťahoch |
Pre úplne štruktúrovanú históriu ťahov (so značkami prerušení,
výzvami na potvrdenie a nespracovanými pozíciami) použite
GET /v1/calls/{call_id}/history.
Rozdiely staršieho payloadu
Starší obal webhooku s jednou adresou URL je
{"type": "telephony.complete" | "web.complete", "data": {…}} bez
event_id a jeho data sa líši od payloadu endpointu:
- Pole ťahov je v
history, nie vtranscripts(rovnaká schéma ťahov ako vyššie). - Sada polí je nespracovaný report po ukončení hovoru a môže zahŕňať ďalšie interné polia nad rámec tabuľky vyššie — neznáme polia považujte za informatívne.
- Webové hovory (
direction: "web") vynechávajúfrom_number/to_numbera pridávajúorigin_domain. - Testovacie hovory z mikrofónu v nástroji Builder sa v staršej ceste hlásia
ako
telephony.complete(systém endpointu ich mapuje naweb.complete). - Koordinácia presmerovania: keď sa hovor skončí presmerovaním, starší
webhook sa volá synchrónne a môže odpovedať
{"transfer_ready": false}, čím signalizuje, že cieľ presmerovania nie je pripravený. Akákoľvek iná odpoveď (alebo žiadny starší webhook) umožní pokračovanie presmerovania. Doručenia endpointu sa na tento účel nikdy nekonzultujú.
Príklad obslužnej funkcie
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 });
},
);
Bežné prípady použitia
Ukladajte prepis každého hovoru a adresu URL nahrávky spolu so záznamami zákazníkov.
Streamujte prepisy do dátového toku na modelovanie tém, extrakciu signálov CSAT alebo monitorovanie miery presmerovaní.
Otvárajte hovory v nástroji QA na kontrolu človekom alebo ich spúšťajte cez vlastný hodnotiaci model.
Pri presmerovaní alebo zlyhaní upozornite člena tímu.