Open in
telephony.complete / web.complete
Neblokujúci webhook doručený po skončení hovoru s prepisom, URL nahrávky a metrikami.
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 vytváranie). Je neblokujúca: odpovedzte ľubovoľným kódom 2xx.
Udalosť sa doručuje oboma spôsobmi:
- Koncové body webhookov prijímajú
telephony.complete(telefonické hovory) aleboweb.complete(webové hovory a testovacie hovory mikrofónu v nástroji na vytváranie) so stabilným údajovým obsahom 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 URL prijíma jeden synchrónny pokus (časový limit 10 s, bez opakovaných pokusov) s mierne odlišným údajovým obsahom — pozrite si Rozdiely v staršom údajovom obsahu.
Údajový obsah požiadavky (doručenia koncovým bodom)
{
"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 | Stabilné vo všetkých udalostiach pre tento hovor |
agent_id | integer | null | Agent, ktorý spracoval hovor, ak bol priradený |
agent_name | string | null | Agent, ktorý spracoval hovor, ak bol priradený |
direction | string | inbound, outbound, web, test. Historické údajové obsahy môžu obsahovať staršie hodnoty mic alebo widget |
from_number, to_number | string | E.164. from_number je doslovná hodnota "web" pre webové a testovacie hovory |
origin_domain | string | Len pre web/test — pôvod stránky, na ktorej bol hostovaný widget (prázdne pre relácie mikrofónu) |
start_time, end_time | timestamp | ISO 8601 UTC |
duration_seconds | integer | null | Odvodené od času začiatku a konca |
status | string | completed alebo failed |
end_reason | string | Pozrite si tabuľku nižšie |
product, voice | string | Konfigurácia agenta platná v čase hovoru |
transfer_number | string | null | Nastavené pri presmerovaní hovoru |
recording_url | string | null | Časovo obmedzená podpísaná URL; súbor stiahnite bezodkladne. null, ak nie je k dispozícii záznam hovoru |
billable_minutes | number | Fakturované minúty zaokrúhlené na najbližšiu štvrtinu minúty (15-sekundové prírastky, minimum 0,25). Hovory priamo do hlasovej schránky tu stále uvádzajú svoje skutočné spoplatnené minúty, ale poplatok je pri sadzbe plánu obmedzený na jednu minútu. |
billing_total_cents | integer | Centy USD |
transcripts | array | Položky prepisu jednotlivých ťahov; môžu byť prázdne, keď prepis nie je k dispozícii |
Dôvody ukončenia
| Hodnota | Význam |
|---|---|
user_hangup | Vzdialená strana zložila ako prvá |
ai_hangup | AI zámerne ukončila hovor |
ai_transfer | AI presmerovala hovor; transfer_number je nastavené |
ai_warm_transfer | AI dokončila teplé (asistované) presmerovanie |
voicemail_hangup | Bola rozpoznaná hlasová schránka a hovor sa ukončil podľa vašej hodnoty voicemail_action |
max_duration | Hovor dosiahol limit maximálneho trvania |
superseded | Relácia bola nahradená novšou reláciou |
unknown | Dôvod ukončenia sa nepodarilo určiť |
Formát prepisu
Každá položka v transcripts predstavuje jeden konverzačný krok. 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 uvedený 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. Uvádza sa, keď je známe časovanie zvuku |
ttfa_ms | celé číslo | Čas do prvého zvuku pre krok model, ak je nameraný |
audio_url, audio_urls | reťazec / pole | Platnosťou obmedzené podpísané adresy URL pre zvuk kroku, ak sa zaznamenáva pre jednotlivé kroky |
Pre úplne štruktúrovanú históriu krokov (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šia obálka webhooku s jednou adresou URL je
{"type": "telephony.complete" | "web.complete", "data": {…}} bez
event_id a jej data sa líši od payloadu koncového bodu:
Starší payload dokončenia obsahuje aj agent_id a agent_name.
- Pole krokov sa nachádza v
history, nie vtranscripts(rovnaká schéma krokov ako vyššie). - Sada polí je nespracovaný report na konci hovoru a môže obsahovať ďalšie interné polia nad rámec tabuľky vyššie — s neznámymi poľami zaobchádzajte ako s informatívnymi.
- Webové hovory (
direction: "web") neobsahujúfrom_number/to_numbera pridávajúorigin_domain. - Hovory testu mikrofónu v nástroji Builder sa v staršej ceste hlásia ako
telephony.complete(systém koncového bodu ich mapuje naweb.complete). - Koordinácia prenosu: keď sa hovor skončí prenosom, starší webhook sa
volá synchrónne a môže odpovedať
{"transfer_ready": false}, aby signalizoval, že cieľ prenosu nie je pripravený. Akákoľvek iná odpoveď (alebo žiadny starší webhook) umožní pokračovanie prenosu. Doručenia koncovému bodu 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 URL nahrávky spolu so záznamami zákazníkov.
Odosielajte prepisy do spracovateľského kanála 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 vo vlastnom modeli hodnotenia.
Pri presmerovaní alebo zlyhaní upozornite člena tímu.