Open in
telephony.complete / web.complete
Webhook neblocant trimis la încheierea unui apel, cu transcrierea, URL-ul înregistrării și metrici.
Un eveniment de finalizare este declanșat după încheierea fiecărui apel — telefonie de intrare, telefonie de ieșire, apel web sau apel de test (sesiune cu microfonul în builder). Acesta nu blochează: răspundeți cu orice cod 2xx.
Evenimentul este livrat prin ambele mecanisme:
- Endpointurile webhook primesc
telephony.complete(apeluri telefonice) sauweb.complete(apeluri web și apeluri de test cu microfonul în builder), cu încărcătura 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 vechi cu un singur URL primește o singură încercare sincronă (timeout de 10 s, fără reîncercări), cu o încărcătură ușor diferită — consultați Diferențe ale încărcăturii vechi.
Corpul cererii (livrări către endpoint)
{
"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",
"extracted_data": {
"status": "completed",
"fields": {
"customer_name": "Alex Morgan",
"appointment_date": "2026-04-23"
},
"evidence": {
"customer_name": {
"quote": "My name is Alex Morgan",
"speaker_role": "caller",
"turn_index": 4
},
"appointment_date": {
"quote": "April 23 works for me",
"speaker_role": "caller",
"turn_index": 7
}
},
"verification": "verified",
"field_reasons": {},
"schema_version": "92850758e231a3c95a..."
},
"from_number": "+14155550199",
"product": "spark",
"recording_stopped_at": null,
"recording_stopped_reason": null,
"recording_url": "https://storage.example.com/…",
"start_time": "2026-04-20T18:24:10.113Z",
"status": "completed",
"summary": "Alex Morgan called to book a follow-up visit and confirmed Thursday, April 23. The agent read the appointment back and Alex asked for a reminder text the day before.",
"to_number": "+15551234567",
"transcripts": [ /* see Transcript format */ ],
"transfer_number": null,
"unresolved_variables": ["campaign_owner"],
"variables": {"campaign_name": "Spring renewals"},
"voice": "john"
},
"event_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
"type": "telephony.complete"
}| Câmp | Tip | Descriere |
|---|---|---|
call_id | integer | Stabil în toate evenimentele pentru acest apel |
agent_id | integer | null | Agentul care a gestionat apelul, atunci când a fost alocat unul |
agent_name | string | null | Agentul care a gestionat apelul, atunci când a fost alocat unul |
direction | string | inbound, outbound, web, test. Încărcăturile istorice pot conține valorile vechi mic sau widget |
from_number, to_number | string | E.164. from_number este valoarea literală "web" pentru apelurile web și apelurile de test |
origin_domain | string | Doar pentru web/test — originea paginii care a găzduit widgetul (goală pentru sesiunile mic) |
start_time, end_time | timestamp | ISO 8601 UTC |
duration_seconds | integer | null | Derivat din ora de început/ora de î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 |
variables | object | Variabile de intrare instantaneizate la începutul apelului |
unresolved_variables | array | Numele variabilelor la care face referire configurația apelului, dar care nu au fost furnizate la începutul apelului |
transfer_number | string | null | Setat când apelul a fost transferat |
recording_url | string | null | URL semnat cu expirare; descărcați-l prompt. null când nu este disponibil niciun artefact de înregistrare |
recording_stopped_at | timestamp | null | Momentul în care înregistrarea a fost oprită înainte de încheierea apelului; nimic de după acest moment nu este inclus în înregistrare. null când înregistrarea a continuat până la finalul apelului sau apelul nu a fost înregistrat |
recording_stopped_reason | string | null | Motivul pentru care înregistrarea s-a oprit anticipat. warm_transfer: se oprește chiar înainte ca un transfer asistat să apeleze destinația și rămâne oprită chiar dacă transferul eșuează și apelantul revine la agent. Tratați celelalte valori ca fiind informative; pot fi adăugate mai multe. null când recording_stopped_at este null |
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ă; poate fi gol când o transcriere nu este disponibilă |
summary | string | null | Rezumat post-apel în limbaj natural (două până la patru propoziții de proză, maximum 2.000 de caractere, redactat în limba apelului: limba principală a agentului, cu excepția cazului în care apelul a suprascris-o) când agentul are activată opțiunea Generați un rezumat al apelului în secțiunea Date de colectat din constructorul de agenți (call_summary_enabled pe Agent) și generarea a reușit; null în caz contrar, inclusiv pentru apeluri prea scurte pentru a fi rezumate, apeluri fără transcriere și agenți cu înregistrarea dezactivată. Rezumatul este generat în aceeași etapă post-apel ca extragerea, astfel că setarea Așteptați datele extrase înainte de a trimite webhookul de finalizare a apelului (extraction_blocks_completion) decide dacă acest eveniment îl așteaptă: într-un mod de așteptare, rezumatul este gata înainte ca acest eveniment să fie trimis (rezumatul însuși primește maximum 30 de secunde din bugetul de 75 de secunde al etapei); când evenimentul nu așteaptă (never sau un transfer în cadrul except_transfers), acest câmp este null, iar rezumatul nu este retrimis în call.data_extracted, așa că citiți-l ulterior din summary pe obiectul Call. De asemenea, disponibil pe pagina apelului din tabloul de bord |
extracted_data | object | null | Rezultat structurat al extragerii cu status, fields, evidence, verification, field_reasons și schema_version. Fiecare câmp nenul are citatul exact verificat structural (maximum 1.000 de caractere), precum și rolul vorbitorului și indexul turei sale; citatele mai lungi returnate de model sunt respinse în loc să fie trunchiate. Dovezile sunt null când câmpul este null. verified înseamnă că fiecare candidat a primit exact un verdict independent valid. unavailable acoperă și ieșirile incorecte sau parțiale ale verificatorului; verdictele parțiale valide se aplică în continuare, în timp ce candidații fără un verdict valid sunt setați la nul. status este completed, failed, exhausted, skipped sau skipped_recording_disabled; null când agentul nu avea câmpuri de extragere |
steps_trace | array | Doar pentru apelurile al căror agent are pași sau transferuri între agenți. Ordinea în care apelul a trecut prin acestea: fiecare intrare are t_ms (milisecunde de la începutul apelului), agent (cheia agentului, a plus ID-ul său), step (ID-ul pasului sau null pentru un transfer către un agent fără pași) și via (start, model, condition, on_enter sau handoff). Conține doar ID-uri și momente, astfel că este păstrat chiar și când înregistrarea este dezactivată |
collected_values | object | Doar pentru apelurile al căror agent are pași sau transferuri între agenți. Valorile colectate de agent în pașii săi, indexate după numele câmpului de colectare, plus <tool>_result pentru rezultatul on_enter_tool al fiecărui pas. Omis când conținutul apelului nu este păstrat (înregistrarea este dezactivată) |
Motive de încheiere
| Valoare | Semnificație |
|---|---|
user_hangup | Partea de la distanță a închis prima |
ai_hangup | Agentul AI a încheiat apelul în mod deliberat |
ai_transfer | Agentul AI a transferat apelul; transfer_number este setat |
ai_warm_transfer | Agentul AI 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ă: limita agentului, propria limită a apelului sau limita platformei de 6 ore. Un apel încheiat de agentul AI sau de apelant după avertismentul privind limita de timp păstrează ai_hangup sau user_hangup |
superseded | Sesiunea a fost înlocuită de una mai nouă |
inbound_config_webhook_failed | Apel primit către un număr în modul webhook: webhook-ul de configurare nu a returnat o configurație utilizabilă, astfel că apelul a fost încheiat înainte ca un agent să răspundă. status este failed, iar apelul nu este facturat |
unknown | Motivul încheierii nu a putut fi determinat |
Formatul transcrierii
Fiecare intrare din transcripts reprezintă un tur conversațional. 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. Sunt prezente când sincronizarea audio este cunoscută |
ttfa_ms | întreg | Timpul până la primul sunet pentru un tur model, când este măsurat |
audio_url, audio_urls | șir / matrice | URL-uri semnate cu expirare pentru sunetul turului, când înregistrarea este realizată pentru fiecare tur |
Pentru istoricul complet structurat al tururilor (cu marcaje de întrerupere,
mesaje de confirmare și poziții brute), utilizați
GET /v1/calls/{call_id}/history.
Diferențe ale payloadului moștenit
Plicul webhook moștenit cu un singur URL este
{"type": "telephony.complete" | "web.complete", "data": {…}} și nu are
niciun event_id, iar câmpul său data diferă de payloadul endpointului:
Payloadul moștenit de finalizare include și agent_id și agent_name.
- Matricea de tururi se află în
history, nu întranscripts(aceeași schemă de tururi ca mai sus). - Setul de câmpuri este raportul brut de final de apel și poate include câmpuri interne suplimentare dincolo 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 endpointului le mapează laweb.complete). - Coordonarea transferului: când un apel se încheie printr-un transfer,
webhookul 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 consultate niciodată pentru aceasta.
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 comune
Păstrați transcrierea și URL-ul înregistrării fiecărui apel alături de datele clienților dumneavoastră.
Transmiteți transcrierile către un pipeline pentru modelarea subiectelor, extragerea semnalelor CSAT sau monitorizarea ratei de transfer.
Deschideți apelurile într-un instrument QA pentru revizuire umană sau procesați-le prin propriul model de evaluare.
Notificați un coleg uman la transfer / eșec.