Open in
telephony.complete / web.complete
Неблокиращ webhook, изпращан при приключване на разговор, с транскрипция, URL адрес на записа и показатели.
Събитие за завършване се задейства след края на всяко обаждане — входящо телефонно обаждане, изходящо телефонно обаждане, уеб обаждане или тестово обаждане (микрофонна сесия в конструктора). То е неблокиращо: отговорете с който и да е 2xx.
Събитието се доставя по двата пътя:
- Webhook крайни точки получават
telephony.complete(телефонни обаждания) илиweb.complete(уеб обаждания и тестови обаждания с микрофон в конструктора) със стабилния payload, документиран по-долу,event_idза всяка доставка, изчакване от 30 s и повторни опити за до 24 h. - Остарелият webhook с един URL получава един синхронен опит (изчакване от 10 s, без повторни опити) с леко различен payload — вижте Разлики в остарелия payload.
Тяло на заявката (доставки до крайни точки)
{
"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"
}| Поле | Тип | Описание |
|---|---|---|
call_id | integer | Постоянен във всяко събитие за това обаждане |
agent_id | integer | null | Агентът, обработил обаждането, когато е бил зададен |
agent_name | string | null | Агентът, обработил обаждането, когато е бил зададен |
direction | string | inbound, outbound, web, test. Историческите тела на заявки може да съдържат остарелите стойности mic или widget |
from_number, to_number | string | E.164. from_number е буквалната стойност "web" за уеб обаждания и тестови обаждания |
origin_domain | string | Само за уеб/тест — произходът на страницата, която хоства уиджета (празно за mic сесии) |
start_time, end_time | timestamp | ISO 8601 UTC |
duration_seconds | integer | null | Изчислява се от началото и края |
status | string | completed или failed |
end_reason | string | Вижте таблицата по-долу |
product, voice | string | Конфигурацията на агента, използвана по време на обаждането |
variables | object | Входни променливи, запазени като моментна снимка при началото на обаждането |
unresolved_variables | array | Имена на променливи, посочени в конфигурацията на обаждането, но непредоставени при започването му |
transfer_number | string | null | Задава се при прехвърляне на обаждането |
recording_url | string | null | Подписан URL с изтичащ срок; изтеглете го своевременно. null, когато няма наличен запис |
billable_minutes | number | Таксувани минути, закръглени до най-близката четвърт минута (интервали от 15 секунди, минимум 0.25). Обажданията, насочени директно към гласова поща, все пак отчитат тук действителните си измерени минути, но таксата е ограничена до една минута по тарифата на плана. |
billing_total_cents | integer | Центове в USD |
transcripts | array | Записи от транскрипцията за всеки ход; може да е празно, когато транскрипция не е налична |
extracted_data | object | null | Резултат от структурирано извличане със status, fields, evidence, verification, field_reasons и schema_version. Всяко поле, различно от null, съдържа точния структурно проверен цитат (до 1 000 знака), както и ролята на говорещия и индекса на хода; по-дългите цитати, върнати от модела, се отхвърлят, вместо да бъдат съкращавани. Evidence е null, когато полето е null. verified означава, че всеки кандидат е получил точно една валидна независима оценка. unavailable обхваща също неправилно форматиран или частичен изход от проверяващия модул; валидните частични оценки все пак се прилагат, а кандидатите без една валидна оценка се задават на null. status е completed, failed, exhausted, skipped или skipped_recording_disabled; null, когато агентът няма полета за извличане |
Причини за приключване
| Стойност | Значение |
|---|---|
user_hangup | Отдалечената страна първа е прекратила разговора |
ai_hangup | AI умишлено е прекратил обаждането |
ai_transfer | AI е прехвърлил обаждането; transfer_number е зададен |
ai_warm_transfer | AI е завършил топло прехвърляне с участник |
voicemail_hangup | Открита е гласова поща и обаждането е прекратено според вашия voicemail_action |
max_duration | Обаждането е достигнало максималното ограничение за продължителност |
superseded | Сесията е заменена с по-нова |
unknown | Причината за приключване не можа да бъде определена |
Формат на транскрипта
Всеки запис в transcripts е един разговорен ход. Ролите са
user (реч на обаждащия се), model (реч на агента и извиквания на инструменти),
tool (резултати от инструменти) и system (събития на разговора, като
превключвания на езика).
[
{
"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"] }
}
}
]| Поле | Тип | Описание |
|---|---|---|
role | низ | user, model, tool или system |
content_type | низ | text/plain за реч; application/json за извиквания на инструменти, резултати от инструменти и системни събития |
content | низ | обект | Текст на речта или структуриран обект като показания по-горе. Извиквания на инструменти: {"tool_call": name, "arguments": {…}}. Резултати от инструменти: {"tool_name": name, "response": {…}} |
start_ms, end_ms | цяло число | Отместване от началото на разговора в ms. Налице, когато е известно времето на аудиото |
ttfa_ms | цяло число | Време до първото аудио за ход на model, когато е измерено |
audio_url, audio_urls | низ / масив | Изтичащи подписани URL адреси за аудиото на хода, когато се записва за всеки ход |
За напълно структурираната история на ходовете (с маркери за прекъсване,
подкани за потвърждение и необработени позиции) използвайте
GET /v1/calls/{call_id}/history.
Разлики в остарелия payload
Остарелият webhook контейнер с един URL е
{"type": "telephony.complete" | "web.complete", "data": {…}} без
event_id, а неговият data се различава от payload-а на endpoint-а:
Остарелият payload при завършване включва също agent_id и agent_name.
- Масивът с ходове е в
history, а не вtranscripts(същата схема на ходовете като по-горе). - Наборът от полета е необработеният отчет в края на разговора и може да включва допълнителни вътрешни полета извън таблицата по-горе — третирайте непознатите полета като информативни.
- Уеб разговорите (
direction: "web") пропускатfrom_number/to_numberи добавятorigin_domain. - Тестовите разговори с микрофон в Builder се отчитат като
telephony.completeпо остарелия път (системата на endpoint-а ги съпоставя сweb.complete). - Координация на прехвърлянето: когато разговор завърши с прехвърляне,
остарелият webhook се извиква синхронно и може да върне
{"transfer_ready": false}, за да сигнализира, че целта на прехвърлянето не е готова. Всеки друг отговор (или липса на остарял webhook) позволява прехвърлянето да продължи. Доставките към endpoint никога не се проверяват за това.
Примерен обработчик
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 });
},
);Често срещани случаи на употреба
Съхранявайте транскрипта и URL адреса на записа на всяко обаждане заедно с данните за клиентите си.
Предавайте транскрипти към конвейер за моделиране на теми, извличане на CSAT сигнали или наблюдение на процента прехвърляния.
Отваряйте обаждания в инструмент за осигуряване на качеството за преглед от човек или ги обработвайте чрез собствен модел за оценяване.
Уведомявайте член на екипа при прехвърляне / неуспех.