telephony.complete / web.complete
Užbaigimo įvykis suaktyvinamas pasibaigus kiekvienam skambučiui — įeinančiam telefono skambučiui, išeinančiam telefono skambučiui, žiniatinklio skambučiui ar bandomajam skambučiui (kūrimo priemonės mikrofono sesijai). Jis yra neblokuojantis: atsakykite bet kuriuo 2xx.
Įvykis pristatomas abiem būdais:
- Webhook galiniai taškai gauna
telephony.complete(telefono skambučiams) arbaweb.complete(žiniatinklio skambučiams ir kūrimo priemonės mikrofono bandomiesiems skambučiams) su toliau dokumentuota stabilia užklausa, kiekvienam pristatymui skirtuevent_id, 30 s skirtuoju laiku ir pakartotiniais bandymais iki 24 val.. - Senstelėjęs vieno URL webhook gauna vieną sinchroninį bandymą (10 s skirtasis laikas, be pakartotinių bandymų) su šiek tiek kitokia užklausa — žr. Senstelėjusios užklausos skirtumai.
Užklausos duomenys (pristatymai į galinius taškus)
{
"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"
}
| Laukas | Tipas | Aprašymas |
|---|---|---|
call_id | integer | Nekinta visuose šio skambučio įvykiuose |
direction | string | inbound, outbound, web, test. Istorinėse užklausose gali būti senstelėjusios mic arba widget reikšmės |
from_number, to_number | string | E.164. Žiniatinklio skambučiams ir bandomiesiems skambučiams from_number yra tiesioginė reikšmė "web" |
origin_domain | string | Tik žiniatinkliui / bandymams — puslapio, kuriame buvo valdiklis, šaltinis (mikrofono sesijoms tuščia) |
start_time, end_time | timestamp | ISO 8601 UTC |
duration_seconds | integer | null | Apskaičiuojama pagal pradžios ir pabaigos laiką |
status | string | completed arba failed |
end_reason | string | Žr. toliau pateiktą lentelę |
product, voice | string | Skambučio metu galiojusi agento konfigūracija |
transfer_number | string | null | Nustatoma, kai skambutis buvo peradresuotas |
recording_url | string | null | Baigianti galioti pasirašyta URL; atsisiųskite nedelsdami. null, kai įrašo artefaktas nepasiekiamas |
billable_minutes | number | Apmokestinamos minutės, suapvalintos iki artimiausio ketvirčio minutės (15 sekundžių intervalai, mažiausiai 0.25). Skambučiai, nukreipti tiesiai į balso paštą, čia vis tiek nurodo faktines pagal naudojimą apskaičiuotas minutes, tačiau mokestis ribojamas iki vienos minutės pagal plano tarifą. |
billing_total_cents | integer | USD centai |
transcripts | array | Kiekvieno pokalbio ėjimo transkripto įrašai; gali būti tuščias, kai transkriptas nepasiekiamas |
Pabaigos priežastys
| Reikšmė | Reikšmė |
|---|---|
user_hangup | Kita šalis pirma nutraukė skambutį |
ai_hangup | Agentas sąmoningai baigė skambutį |
ai_transfer | Agentas peradresavo skambutį; nustatytas transfer_number |
ai_warm_transfer | Agentas užbaigė šiltąjį (su dalyvavimu) peradresavimą |
voicemail_hangup | Aptiktas balso paštas ir skambutis baigtas pagal jūsų voicemail_action |
max_duration | Skambutis pasiekė maksimalią trukmės ribą |
superseded | Sesiją pakeitė naujesnė sesija |
unknown | Nepavyko nustatyti pabaigos priežasties |
Nuorašo formatas
Kiekvienas transcripts įrašas yra vienas pokalbio etapas. Vaidmenys yra
user (skambinančiojo kalba), model (balso agento kalba ir įrankių iškvietimai),
tool (įrankių rezultatai) ir system (skambučio įvykiai, pvz., kalbos
perjungimai).
[
{
"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"] }
}
}
]
| Laukas | Tipas | Aprašymas |
|---|---|---|
role | eilutė | user, model, tool arba system |
content_type | eilutė | text/plain kalbai; application/json įrankių iškvietimams, įrankių rezultatams ir sistemos įvykiams |
content | eilutė | objektas | Kalbos tekstas arba pirmiau pateiktas struktūrizuotas objektas. Įrankių iškvietimai: {"tool_call": name, "arguments": {…}}. Įrankių rezultatai: {"tool_name": name, "response": {…}} |
start_ms, end_ms | sveikasis skaičius | Poslinkiai nuo skambučio pradžios milisekundėmis. Pateikiami, kai žinomas garso laikas |
ttfa_ms | sveikasis skaičius | Laikas iki pirmojo garso model etape, kai išmatuotas |
audio_url, audio_urls | eilutė / masyvas | Baigiančio galioti pasirašyti etapo garso URL, kai garsas įrašomas kiekvienam etapui |
Jei reikia visos struktūrizuotos etapų istorijos (su pertraukimo žymomis,
patvirtinimo raginimais ir neapdorotomis pozicijomis), naudokite
GET /v1/calls/{call_id}/history.
Senesnio paketo skirtumai
Senesnės versijos vieno URL žiniatinklio kabliuko paketo apvalkalas yra
{"type": "telephony.complete" | "web.complete", "data": {…}} be
event_id, o jo data skiriasi nuo galinio taško paketo:
- Etapų masyvas yra
history, o netranscripts(tokia pati etapų schema kaip pirmiau). - Laukų rinkinys yra neapdorota skambučio pabaigos ataskaita ir gali apimti papildomus vidinius laukus, nepateiktus pirmiau esančioje lentelėje — nežinomus laukus laikykite informaciniais.
- Žiniatinklio skambučiai (
direction: "web") neįtraukiafrom_number/to_numberir pridedaorigin_domain. - Konstruktoriaus mikrofono bandomieji skambučiai senesniame kelyje pateikiami kaip
telephony.complete(galinio taško sistema juos susieja suweb.complete). - Peradresavimo koordinavimas: kai skambutis baigiasi peradresavimu, senasis
žiniatinklio kabliukas iškviečiamas sinchroniškai ir gali atsakyti
{"transfer_ready": false}, kad nurodytų, jog peradresavimo paskirties vieta dar nepasirengusi. Bet koks kitas atsakymas (arba senojo žiniatinklio kabliuko nebuvimas) leidžia tęsti peradresavimą. Galinio taško pristatymai šiuo tikslu niekada nenaudojami.
Pavyzdinė apdorojimo funkcija
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 });
},
);
Dažniausi naudojimo atvejai
Išsaugokite kiekvieno skambučio nuorašą ir įrašo URL kartu su klientų įrašais.
Siųskite nuorašus į duomenų srautą temų modeliavimui, CSAT signalų išgavimui arba peradresavimo dažnio stebėjimui.
Atidarykite skambučius kokybės užtikrinimo įrankyje žmogaus peržiūrai arba apdorokite juos savo vertinimo modeliu.
Peradresavimo arba nesėkmės atveju informuokite komandos narį.