telephony.complete / web.complete
Събитие за завършване се задейства след края на всяко обаждане — входяща телефония, изходяща телефония, уеб обаждане или тестово обаждане (микрофонна сесия в конструктора). То е неблокиращо: отговорете с произволен 2xx.
Събитието се доставя по двата пътя:
- Крайни точки за уебхуки получават
telephony.complete(телефонни обаждания) илиweb.complete(уеб обаждания и тестови обаждания с микрофон в конструктора) със стабилния полезен товар, документиран по-долу,event_idза всяка доставка, изчакване от 30 s и повторни опити до 24 h. - Наследеният уебхук с един URL получава един синхронен опит (изчакване от 10 s, без повторни опити) с малко по-различен полезен товар — вижте Разлики в наследения полезен товар.
Полезен товар на заявката (доставки към крайни точки)
{
"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"
}
| Поле | Тип | Описание |
|---|---|---|
call_id | integer | Стабилен във всяко събитие за това обаждане |
direction | string | inbound, outbound, web, test. Историческите полезни товари може да съдържат наследени стойности mic или widget |
from_number, to_number | string | E.164. from_number е буквалният "web" за уеб обаждания и тестови обаждания |
origin_domain | string | Само за уеб/тест — произходът на страницата, която хоства джаджата (празно за микрофонни сесии) |
start_time, end_time | timestamp | ISO 8601 UTC |
duration_seconds | integer | null | Извежда се от началния и крайния момент |
status | string | completed или failed |
end_reason | string | Вижте таблицата по-долу |
product, voice | string | Конфигурацията на агента, активна към момента на обаждането |
transfer_number | string | null | Задава се, когато обаждането е прехвърлено |
recording_url | string | null | Подписан URL с изтичащ срок; изтеглете своевременно. null, когато няма наличен запис |
billable_minutes | number | Таксувани минути, закръглени до най-близката четвърт минута (интервали от 15 секунди, минимум 0.25). Обажданията, пренасочени директно към гласова поща, все пак отчитат тук действителните си измерени минути, но таксата е ограничена до една минута по тарифата на плана. |
billing_total_cents | integer | Американски центове |
transcripts | array | Записи от транскрипцията за всеки ход; може да е празно, когато транскрипция не е налична |
Причини за прекратяване
| Стойност | Значение |
|---|---|
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 | string | user, model, tool или system |
content_type | string | text/plain за реч; application/json за извиквания на инструменти, резултати от инструменти и системни събития |
content | string | object | Текст на речта или структуриран обект, показан по-горе. Извиквания на инструменти: {"tool_call": name, "arguments": {…}}. Резултати от инструменти: {"tool_name": name, "response": {…}} |
start_ms, end_ms | integer | Отмествания от началото на разговора, в ms. Налични, когато е известно времето на аудиото |
ttfa_ms | integer | Време до първото аудио за ход на model, когато е измерено |
audio_url, audio_urls | string / array | Подписани URL адреси с изтичащ срок за аудиото на хода, когато е записано за всеки ход |
За пълната структурирана история на ходовете (с маркери за прекъсване,
подкани за потвърждение и необработени позиции) използвайте
GET /v1/calls/{call_id}/history.
Разлики при стария payload
Старият webhook контейнер с един URL адрес е
{"type": "telephony.complete" | "web.complete", "data": {…}} без
event_id, а неговият data се различава от payload-а на endpoint-а:
- Масивът с ходове е под
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 сигнали или наблюдение на дела на прехвърлянията.
Отваряйте обажданията в инструмент за осигуряване на качеството за преглед от човек или ги обработвайте чрез собствен модел за оценяване.
Уведомявайте член на екипа при прехвърляне / неуспех.