telephony.complete / web.complete
Webhook neblocant transmis la încheierea unui apel, cu transcriere, URL-ul înregistrării și metrici.
Un eveniment de finalizare se declanșează după încheierea fiecărui apel — apel telefonic de intrare, apel telefonic de ieșire, apel web sau apel de test (sesiune cu microfonul din generator). Acesta este neblocant: răspundeți cu orice cod 2xx.
Evenimentul este livrat pe ambele căi:
- Endpointurile webhook primesc
telephony.complete(apeluri telefonice) sauweb.complete(apeluri web și apeluri de test cu microfonul din generator) cu payloadul stabil documentat mai jos, unevent_idpentru fiecare livrare, un timeout de 30 s și reîncercări timp de până la 24 h. - Webhookul unic cu URL moștenit primește o singură încercare sincronă (timeout de 10 s, fără reîncercări) cu un payload ușor diferit — consultați Diferențe ale payloadului moștenit.
Payloadul cererii (livrări către endpointuri)
{
"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"
}| Câmp | Tip | Descriere |
|---|---|---|
call_id | integer | Stabil pentru fiecare eveniment al acestui apel |
direction | string | inbound, outbound, web, test. Payloadurile istorice pot conține valorile moștenite mic sau widget |
from_number, to_number | string | E.164. from_number este literal "web" pentru apelurile web și apelurile de test |
origin_domain | string | Numai web/test — originea paginii care a găzduit widgetul (gol pentru sesiunile cu microfonul) |
start_time, end_time | timestamp | ISO 8601 UTC |
duration_seconds | integer | null | Derivat din ora de început/încheiere |
status | string | completed sau failed |
end_reason | string | Consultați tabelul de mai jos |
product, voice | string | Configurația agentului activă la momentul apelului |
transfer_number | string | null | Setat când apelul a fost transferat |
recording_url | string | null | URL semnat cu expirare; descărcați prompt. null când nu este disponibil niciun artefact de înregistrare |
billable_minutes | number | Minute facturate, rotunjite la cel mai apropiat sfert de minut (incrementări de 15 secunde, minimum 0.25). Apelurile redirecționate direct către mesageria vocală raportează în continuare aici minutele lor efective contorizate, însă taxa este plafonată la un minut la tariful planului. |
billing_total_cents | integer | Cenți USD |
transcripts | array | Intrări de transcriere pentru fiecare tură; pot fi goale când o transcriere nu este disponibilă |
Motive de încheiere
| Valoare | Semnificație |
|---|---|
user_hangup | Partea de la distanță a închis prima |
ai_hangup | IA a încheiat apelul în mod deliberat |
ai_transfer | IA a transferat apelul; transfer_number este setat |
ai_warm_transfer | IA a finalizat un transfer asistat |
voicemail_hangup | A fost detectată mesageria vocală, iar apelul s-a încheiat conform voicemail_action |
max_duration | Apelul a atins limita maximă de durată |
superseded | Sesiunea a fost înlocuită de una mai nouă |
unknown | Motivul încheierii nu a putut fi determinat |
Formatul transcrierii
Fiecare intrare din transcripts reprezintă un tur de conversație. Rolurile sunt
user (vorbirea apelantului), model (vorbirea agentului și apelurile de instrumente),
tool (rezultatele instrumentelor) și system (evenimente ale apelului, cum ar fi
schimbările de limbă).
[
{
"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"] }
}
}
]| Câmp | Tip | Descriere |
|---|---|---|
role | șir | user, model, tool sau system |
content_type | șir | text/plain pentru vorbire; application/json pentru apeluri de instrumente, rezultate ale instrumentelor și evenimente de sistem |
content | șir | obiect | Textul vorbirii sau obiectul structurat prezentat mai sus. Apeluri de instrumente: {"tool_call": name, "arguments": {…}}. Rezultate ale instrumentelor: {"tool_name": name, "response": {…}} |
start_ms, end_ms | întreg | Decalaje față de începutul apelului, în ms. Prezente când sincronizarea audio este cunoscută |
ttfa_ms | întreg | Timpul până la primul fragment audio pentru un tur model, când este măsurat |
audio_url, audio_urls | șir / matrice | URL-uri semnate cu expirare pentru conținutul audio al turului, când sunt înregistrate pentru fiecare tur |
Pentru istoricul complet structurat al tururilor (cu marcaje de întrerupere,
prompturi de confirmare și poziții brute), utilizați
GET /v1/calls/{call_id}/history.
Diferențe ale încărcăturii utile moștenite
Învelișul webhook moștenit cu un singur URL este
{"type": "telephony.complete" | "web.complete", "data": {…}}, fără
event_id, iar data diferă de încărcătura utilă a endpointului:
- Matricea de tururi se află în
history, nu întranscripts(aceeași schemă de tur ca mai sus). - Setul de câmpuri reprezintă raportul brut de final de apel și poate include câmpuri interne suplimentare față de tabelul de mai sus — tratați câmpurile necunoscute ca informaționale.
- Apelurile web (
direction: "web") omitfrom_number/to_numberși adaugăorigin_domain. - Apelurile de testare a microfonului din Builder sunt raportate ca
telephony.completepe calea moștenită (sistemul endpoint le mapează laweb.complete). - Coordonarea transferului: când un apel se încheie printr-un transfer,
webhook-ul moștenit este apelat sincron și poate răspunde cu
{"transfer_ready": false}pentru a semnala că ținta transferului nu este pregătită. Orice alt răspuns (sau lipsa unui webhook moștenit) permite continuarea transferului. Livrările către endpoint nu sunt niciodată consultate în acest scop.
Exemplu de handler
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 });
},
);Cazuri de utilizare frecvente
Stocați transcrierea și URL-ul înregistrării fiecărui apel alături de datele clienților.
Transmiteți transcrierile către un flux pentru modelarea subiectelor, extragerea semnalelor CSAT sau monitorizarea ratei de transfer.
Deschideți apelurile într-un instrument QA pentru verificare umană sau procesați-le prin propriul model de evaluare.
Notificați un coleg uman în cazul unui transfer / eșec.