telephony.complete / web.complete
Niet-blokkerende webhook die wordt verzonden wanneer een oproep eindigt, met transcript, opname-URL en statistieken.
Een voltooiingsgebeurtenis wordt geactiveerd nadat elke oproep is beëindigd — inkomende telefonie, uitgaande telefonie, weboproep of testoproep (microfoonsessie in de builder). Deze is niet-blokkerend: reageer met een willekeurige 2xx-status.
De gebeurtenis wordt via beide paden geleverd:
- Webhook-eindpunten ontvangen
telephony.complete(telefoongesprekken) ofweb.complete(weboproepen en microfoontestoproepen in de builder) met de stabiele payload die hieronder is gedocumenteerd, eenevent_idper aflevering, een time-out van 30 s en nieuwe pogingen gedurende maximaal 24 h. - De verouderde webhook met één URL ontvangt één synchrone poging (time-out van 10 s, geen nieuwe pogingen) met een enigszins andere payload — zie Verschillen in verouderde payloads.
Requestpayload (leveringen aan eindpunten)
{
"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"
}| Veld | Type | Beschrijving |
|---|---|---|
call_id | integer | Stabiel voor elke gebeurtenis van deze oproep |
direction | string | inbound, outbound, web, test. Historische payloads kunnen verouderde waarden mic of widget bevatten |
from_number, to_number | string | E.164. from_number is letterlijk "web" voor weboproepen en testoproepen |
origin_domain | string | Alleen web/test — de oorsprong van de pagina waarop de widget werd gehost (leeg voor microfoonsessies) |
start_time, end_time | timestamp | ISO 8601 UTC |
duration_seconds | integer | null | Afgeleid van begin/einde |
status | string | completed of failed |
end_reason | string | Zie de onderstaande tabel |
product, voice | string | Agentconfiguratie die actief was tijdens de oproep |
transfer_number | string | null | Ingesteld wanneer de oproep is doorgeschakeld |
recording_url | string | null | Ondertekende URL met vervaldatum; download deze tijdig. null wanneer geen opnameartefact beschikbaar is |
billable_minutes | number | Gefactureerde minuten, afgerond op het dichtstbijzijnde kwartier minuut (stappen van 15 seconden, minimaal 0.25). Oproepen die rechtstreeks naar voicemail gaan, rapporteren hier nog steeds hun werkelijke gemeten minuten, maar de kosten zijn gemaximeerd op één minuut tegen het pakkettarief. |
billing_total_cents | integer | Amerikaanse centen |
transcripts | array | Transcriptvermeldingen per beurt; kan leeg zijn wanneer een transcript niet beschikbaar is |
Eindredenen
| Waarde | Betekenis |
|---|---|
user_hangup | Externe partij heeft als eerste opgehangen |
ai_hangup | AI heeft de oproep bewust beëindigd |
ai_transfer | AI heeft de oproep doorgeschakeld; transfer_number is ingesteld |
ai_warm_transfer | AI heeft een warme (begeleide) doorschakeling voltooid |
voicemail_hangup | Voicemail is gedetecteerd en de oproep is beëindigd volgens je voicemail_action |
max_duration | Oproep heeft de maximale tijdslimiet bereikt |
superseded | De sessie is vervangen door een nieuwere |
unknown | Eindreden kon niet worden vastgesteld |
Transcriptindeling
Elke invoer in transcripts is één gespreksbeurt. Rollen zijn
user (spraak van de beller), model (spraak van de agent en toolaanroepen),
tool (toolresultaten) en system (oproepgebeurtenissen, zoals taalwisselingen).
[
{
"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"] }
}
}
]| Veld | Type | Beschrijving |
|---|---|---|
role | string | user, model, tool of system |
content_type | string | text/plain voor spraak; application/json voor toolaanroepen, toolresultaten en systeemgebeurtenissen |
content | string | object | Spraaktekst, of het hierboven weergegeven gestructureerde object. Toolaanroepen: {"tool_call": name, "arguments": {…}}. Toolresultaten: {"tool_name": name, "response": {…}} |
start_ms, end_ms | integer | Offsets vanaf het begin van de oproep, in ms. Aanwezig wanneer de audiotiming bekend is |
ttfa_ms | integer | Tijd tot eerste audio voor een model-beurt, wanneer gemeten |
audio_url, audio_urls | string / array | Verlopende ondertekende URL's voor de audio van de beurt, wanneer deze per beurt wordt opgenomen |
Gebruik voor de volledig gestructureerde beurtgeschiedenis (met onderbrekingsmarkeringen,
bevestigingsprompts en onbewerkte posities)
GET /v1/calls/{call_id}/history.
Verschillen in legacy-payloads
De legacy-webhookenvelop met één URL is
{"type": "telephony.complete" | "web.complete", "data": {…}} zonder
event_id, en de data verschilt van de endpoint-payload:
- De beurtarray staat onder
history, niet ondertranscripts(hetzelfde beurtschema als hierboven). - De veldenset is het onbewerkte rapport aan het einde van de oproep en kan aanvullende interne velden bevatten naast de bovenstaande tabel — behandel onbekende velden als informatief.
- Weboproepen (
direction: "web") latenfrom_number/to_numberweg en voegenorigin_domaintoe. - Mic-testoproepen in de Builder worden op het legacy-pad gerapporteerd als
telephony.complete(het endpointsysteem wijst ze toe aanweb.complete). - Overdrachtscoördinatie: wanneer een oproep eindigt met een overdracht, wordt de
legacy-webhook synchroon aangeroepen en kan deze
{"transfer_ready": false}retourneren om aan te geven dat het overdrachtsdoel nog niet klaar is. Elke andere reactie (of geen legacy-webhook) laat de overdracht doorgaan. Endpoint-leveringen worden hiervoor nooit geraadpleegd.
Voorbeeldhandler
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 });
},
);Veelvoorkomende toepassingen
Sla het transcript en de opname-URL van elke oproep op naast je klantgegevens.
Stream transcripties naar een pipeline voor onderwerpmodellering, CSAT-signaalextractie of monitoring van het doorverbindingspercentage.
Open oproepen in een QA-tool voor menselijke beoordeling, of voer ze uit met je eigen evaluatiemodel.
Waarschuw een menselijk teamlid bij doorverbinding / mislukking.