Open in
telephony.complete / web.complete
Neblokirajući webhook koji se isporučuje kada poziv završi, s transkriptom, URL-om snimke i metrikama.
Događaj dovršetka pokreće se nakon završetka svakog poziva — dolazne telefonije, odlazne telefonije, web-poziva ili testnog poziva (sesija mikrofona u alatu za izradu). On je neblokirajući: odgovorite bilo kojim statusom 2xx.
Događaj se isporučuje na oba načina:
- Krajnje točke webhooka primaju
telephony.complete(telefonski pozivi) iliweb.complete(web-pozivi i testni pozivi mikrofonom u alatu za izradu) sa stabilnim teretom dokumentiranim u nastavku, zasebnimevent_idza svaku isporuku, vremenskim ograničenjem od 30 s i ponovnim pokušajima do 24 h. - Naslijeđeni webhook s jednim URL-om prima jedan sinkroni pokušaj (vremensko ograničenje od 10 s, bez ponovnih pokušaja) s neznatno drukčijim teretom — pogledajte Razlike u naslijeđenom teretu.
Tijelo zahtjeva (isporuke krajnjoj točki)
{
"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_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,
"unresolved_variables": ["campaign_owner"],
"variables": {"campaign_name": "Spring renewals"},
"voice": "john"
},
"event_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
"type": "telephony.complete"
}| Polje | Vrsta | Opis |
|---|---|---|
call_id | integer | Stabilan u svim događajima za ovaj poziv |
agent_id | integer | null | Agent koji je obradio poziv, ako je dodijeljen |
agent_name | string | null | Agent koji je obradio poziv, ako je dodijeljen |
direction | string | inbound, outbound, web, test. Povijesna tijela podataka mogu sadržavati zastarjele vrijednosti mic ili widget |
from_number, to_number | string | E.164. from_number je doslovna vrijednost "web" za web-pozive i testne pozive |
origin_domain | string | Samo za web/test — ishodište stranice na kojoj je smješten widget (prazno za mikrofonske sesije) |
start_time, end_time | timestamp | ISO 8601 UTC |
duration_seconds | integer | null | Izvedeno iz vremena početka/završetka |
status | string | completed ili failed |
end_reason | string | Pogledajte tablicu u nastavku |
product, voice | string | Konfiguracija agenta aktivna u trenutku poziva |
variables | object | Ulazne varijable zabilježene pri početku poziva |
unresolved_variables | array | Nazivi varijabli na koje se poziva konfiguracija poziva, ali nisu zadane pri početku poziva |
transfer_number | string | null | Postavljeno kada je poziv preusmjeren |
recording_url | string | null | Potpisani URL s istekom; preuzmite odmah. null kada artefakt snimke nije dostupan |
billable_minutes | number | Naplativi minuti, zaokruženi na najbližu četvrtinu minute (koraci od 15 sekundi, najmanje 0.25). Pozivi koji idu izravno na govornu poštu i dalje ovdje prijavljuju stvarne mjerene minute, ali se naknada ograničava na jednu minutu po cijeni plana. |
billing_total_cents | integer | Američki centi |
transcripts | array | Unosi prijepisa po izmjeni; mogu biti prazni kada prijepis nije dostupan |
extracted_data | object | null | Rezultat strukturiranog izdvajanja sa stavkama status, fields, evidence, verification, field_reasons i schema_version. Svako polje koje nije null sadrži točan strukturno provjeren citat (najviše 1.000 znakova), kao i ulogu govornika i indeks izmjene; dulji citati koje vrati model odbacuju se umjesto da se skraćuju. Dokaz je null kada je polje null. verified znači da je svaki kandidat dobio točno jednu valjanu neovisnu presudu. unavailable također obuhvaća neispravan ili djelomičan izlaz provjeravatelja; valjane djelomične presude i dalje se primjenjuju, dok se kandidati bez jedne valjane presude postavljaju na null. status je completed, failed, exhausted, skipped ili skipped_recording_disabled; null kada agent nije imao polja za izdvajanje |
Razlozi završetka
| Vrijednost | Značenje |
|---|---|
user_hangup | Udaljena strana prva je prekinula poziv |
ai_hangup | AI je namjerno završio poziv |
ai_transfer | AI je preusmjerio poziv; transfer_number je postavljen |
ai_warm_transfer | AI je dovršio toplo (nadzirano) preusmjeravanje |
voicemail_hangup | Otkrivena je govorna pošta, a poziv je završen prema vašem voicemail_action |
max_duration | Poziv je dosegnuo ograničenje maksimalnog trajanja |
superseded | Sesiju je zamijenila novija sesija |
unknown | Razlog završetka nije se mogao utvrditi |
Format transkripta
Svaki unos u transcripts jedna je razgovorna izmjena. Uloge su
user (govor pozivatelja), model (govor agenta i pozivi alata),
tool (rezultati alata) i system (događaji poziva, kao što su
promjene jezika).
[
{
"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"] }
}
}
]| Polje | Vrsta | Opis |
|---|---|---|
role | niz znakova | user, model, tool ili system |
content_type | niz znakova | text/plain za govor; application/json za pozive alata, rezultate alata i događaje sustava |
content | niz znakova | objekt | Tekst govora ili strukturirani objekt prikazan iznad. Pozivi alata: {"tool_call": name, "arguments": {…}}. Rezultati alata: {"tool_name": name, "response": {…}} |
start_ms, end_ms | cijeli broj | Pomaci od početka poziva, u ms. Prisutno kada je poznato vremensko usklađivanje zvuka |
ttfa_ms | cijeli broj | Vrijeme do prvog zvuka za izmjenu model, kada je izmjereno |
audio_url, audio_urls | niz znakova / polje | Istječući potpisani URL-ovi za zvuk izmjene, kada se snima po izmjeni |
Za potpuno strukturiranu povijest izmjena (s oznakama prekida,
upitima za potvrdu i izvornim položajima) upotrijebite
GET /v1/calls/{call_id}/history.
Razlike u naslijeđenom payloadu
Naslijeđena omotnica webhooka s jednim URL-om jest
{"type": "telephony.complete" | "web.complete", "data": {…}} bez
event_id, a njezin se data razlikuje od payloada krajnje točke:
Naslijeđeni payload dovršetka također sadrži agent_id i agent_name.
- Polje izmjena nalazi se pod
history, a ne podtranscripts(s istom shemom izmjena kao gore). - Skup polja neobrađeno je izvješće o završetku poziva i može sadržavati dodatna interna polja osim onih u tablici iznad — nepoznata polja tretirajte kao informativna.
- Web-pozivi (
direction: "web") izostavljajufrom_number/to_numberi dodajuorigin_domain. - Pozivi za test mikrofona u Builderu prijavljuju se kao
telephony.completena naslijeđenoj putanji (sustav krajnje točke mapira ih naweb.complete). - Koordinacija preusmjeravanja: kada poziv završi preusmjeravanjem,
naslijeđeni webhook poziva se sinkrono i može odgovoriti
{"transfer_ready": false}kako bi signalizirao da odredište preusmjeravanja nije spremno. Svaki drugi odgovor (ili izostanak naslijeđenog webhooka) omogućuje nastavak preusmjeravanja. Isporuke krajnjoj točki nikada se ne provjeravaju za to.
Primjer rukovatelja
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 });
},
);Uobičajeni slučajevi upotrebe
Pohranite transkript svakog poziva i URL snimke uz evidenciju svojih korisnika.
Usmjerite transkripte u kanal za modeliranje tema, izdvajanje CSAT signala ili praćenje stope prijenosa.
Otvorite pozive u alatu za osiguranje kvalitete radi ljudske provjere ili ih obradite svojim modelom za evaluaciju.
Aktivirajte ljudskog člana tima pri prijenosu / neuspjehu.