Open in
telephony.complete / web.complete
Неблокувальний вебхук, що надсилається після завершення дзвінка, із транскриптом, URL запису та метриками.
Подія завершення спрацьовує після завершення кожного дзвінка — вхідної телефонії, вихідної телефонії, вебдзвінка або тестового дзвінка (сеанс мікрофона в конструкторі). Вона неблокувальна: надішліть будь-яку відповідь 2xx.
Подія доставляється обома шляхами:
- Кінцеві точки вебхуків отримують
telephony.complete(телефонні дзвінки) абоweb.complete(вебдзвінки та тестові дзвінки з мікрофона в конструкторі) зі стабільним навантаженням, описаним нижче,event_idдля кожної доставки, тайм-аутом 30 с і повторними спробами протягом до 24 год. - Застарілий вебхук з єдиною URL-адресою отримує одну синхронну спробу (тайм-аут 10 с, без повторних спроб) із дещо іншим навантаженням — див. Відмінності застарілого навантаження.
Тіло запиту (доставки до ендпойнта)
{
"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"
}| Поле | Тип | Опис |
|---|---|---|
call_id | ціле число | Незмінний для кожної події цього виклику |
agent_id | ціле число | null | Агент, який обробив виклик, якщо його було призначено |
agent_name | рядок | null | Агент, який обробив виклик, якщо його було призначено |
direction | рядок | inbound, outbound, web, test. Історичні корисні навантаження можуть містити застарілі значення mic або widget |
from_number, to_number | рядок | E.164. Для вебвикликів і тестових викликів from_number має буквальне значення "web" |
origin_domain | рядок | Лише для вебвикликів/тестів — джерело сторінки, на якій розміщено віджет (порожнє для сесій mic) |
start_time, end_time | мітка часу | ISO 8601 UTC |
duration_seconds | ціле число | null | Визначається на основі часу початку й завершення |
status | рядок | completed або failed |
end_reason | рядок | Див. таблицю нижче |
product, voice | рядок | Конфігурація агента, що діяла під час виклику |
variables | об’єкт | Вхідні змінні, знімок яких створено на початку виклику |
unresolved_variables | масив | Назви змінних, на які посилається конфігурація виклику, але які не було передано на початку виклику |
transfer_number | рядок | null | Установлюється, коли виклик було переведено |
recording_url | рядок | null | Тимчасове підписане URL-адреса; завантажте негайно. null, якщо запис недоступний |
recording_stopped_at | мітка часу | null | Час, коли запис було зупинено до завершення виклику; у записі немає нічого після цього моменту. null, якщо запис тривав до завершення виклику або виклик не записувався |
recording_stopped_reason | рядок | null | Причина дострокового зупинення запису. warm_transfer: запис зупиняється безпосередньо перед тим, як тепле переведення телефонує адресату, і не відновлюється, навіть якщо переведення не вдалося та абонент повернувся до агента. Інші значення вважайте інформаційними; їх може бути додано більше. null, якщо recording_stopped_at має значення null |
billable_minutes | число | Хвилини, за які виставлено рахунок, округлені до найближчої чверті хвилини (інтервали по 15 секунд, мінімум 0.25). Виклики з прямим переходом на голосову пошту все одно повідомляють тут фактичні виміряні хвилини, але плата обмежена однією хвилиною за тарифом плану. |
billing_total_cents | ціле число | Центи USD |
transcripts | масив | Записи транскрипту для кожної репліки; можуть бути порожніми, якщо транскрипт недоступний |
summary | рядок | null | Підсумок після виклику простою мовою (від двох до чотирьох речень прозового тексту, не більше 2 000 символів, мовою виклику: основною мовою агента, якщо виклик не змінив її), якщо в конструкторі агента для параметра Створювати підсумок виклику в розділі Дані для збору увімкнено відповідну опцію (call_summary_enabled у Agent) і генерація успішно завершилася; в іншому разі — null, зокрема для викликів, надто коротких для підсумовування, викликів без транскрипту та агентів із вимкненим записом. Підсумок генерується на тому самому етапі після виклику, що й витягування даних, тому налаштування Очікувати витягнуті дані перед надсиланням вебхука про завершення виклику (extraction_blocks_completion) визначає, чи очікує ця подія на нього: у режимі очікування підсумок готовий до надсилання цієї події (сам підсумок отримує не більше 30 секунд у межах 75-секундного бюджету етапу); якщо подія не очікує (never або переведення за except_transfers), це поле має значення null, а підсумок не надсилається повторно в call.data_extracted, тому прочитайте його пізніше з summary в об’єкті Call. Також доступний на сторінці виклику в панелі керування |
extracted_data | об’єкт | null | Структурований результат витягування з status, fields, evidence, verification, field_reasons і schema_version. Кожне поле, що не має значення null, містить точну структурно перевірену цитату (не більше 1 000 символів), а також роль мовця й індекс репліки; довші цитати, повернуті моделлю, відхиляються, а не скорочуються. Доказ має значення null, якщо поле має значення null. verified означає, що кожен кандидат отримав рівно один дійсний незалежний вердикт. unavailable також охоплює некоректний або частковий вивід верифікатора; дійсні часткові вердикти все одно застосовуються, тоді як кандидатам без одного дійсного вердикту встановлюється значення null. status має значення completed, failed, exhausted, skipped або skipped_recording_disabled; null, якщо агент не мав полів для витягування |
steps_trace | масив | Лише для викликів, агент яких має кроки або передачі. Порядок, у якому виклик проходив через них: кожен запис містить t_ms (мілісекунди від початку виклику), agent (ключ агента: a плюс його ID), step (ID кроку або null під час передачі агенту без кроків) і via (start, model, condition, on_enter або handoff). Містить лише ID і час, тому зберігається навіть за вимкненого запису |
collected_values | об’єкт | Лише для викликів, агент яких має кроки або передачі. Значення, які агент зібрав на своїх кроках, із ключами за назвою поля збору, а також <tool>_result для результату on_enter_tool кожного кроку. Не включається, якщо вміст виклику не зберігається (запис вимкнено) |
Причини завершення
| Значення | Значення |
|---|---|
user_hangup | Віддалена сторона завершила виклик першою |
ai_hangup | ШІ навмисно завершив виклик |
ai_transfer | ШІ переадресував виклик; transfer_number задано |
ai_warm_transfer | ШІ виконав тепле переадресування з участю агента |
voicemail_hangup | Було виявлено голосову пошту, і виклик завершено відповідно до вашого voicemail_action |
max_duration | Виклик досяг максимального обмеження тривалості: обмеження агента, самого виклику або обмеження платформи в 6 годин. Виклик, який ШІ або абонент завершує після попередження про обмеження часу, зберігає значення ai_hangup або user_hangup |
superseded | Сеанс було замінено новішим |
inbound_config_webhook_failed | Вхідний виклик на номер у режимі вебхука: вебхук конфігурації не повернув придатну конфігурацію, тому виклик було завершено до відповіді агента. status має значення failed, і виклик не тарифікується |
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 | ціле число | Зміщення від початку виклику в мс. Наявні, коли відомий час аудіо |
ttfa_ms | ціле число | Час до першого аудіо для ходу model, якщо виміряно |
audio_url, audio_urls | рядок / масив | Тимчасові підписані URL-адреси для аудіо ходу, якщо запис ведеться для кожного ходу окремо |
Для повністю структурованої історії ходів (із маркерами переривань,
підказками підтвердження та необробленими позиціями) використовуйте
GET /v1/calls/{call_id}/history.
Відмінності застарілого payload
Застарілий конверт вебхука з однією URL-адресою має вигляд
{"type": "telephony.complete" | "web.complete", "data": {…}} і
не містить event_id, а його data відрізняється від payload кінцевої точки:
Застарілий payload завершення також містить agent_id і agent_name.
- Масив ходів міститься у
history, а не вtranscripts(та сама схема ходів, що й вище). - Набір полів є необробленим звітом наприкінці виклику й може містити додаткові внутрішні поля поза наведеною вище таблицею — розглядайте невідомі поля як інформаційні.
- Вебвиклики (
direction: "web") не містятьfrom_number/to_numberі додаютьorigin_domain. - Виклики тестування мікрофона в Builder передаються як
telephony.completeу застарілому шляху (система кінцевої точки зіставляє їх ізweb.complete). - Координація переадресації: коли виклик завершується переадресацією,
застарілий вебхук викликається синхронно та може відповісти
{"transfer_ready": false}, щоб повідомити, що ціль переадресації ще не готова. Будь-яка інша відповідь (або відсутність застарілого вебхука) дозволяє переадресації продовжитися. Доставки кінцевої точки для цього ніколи не використовуються.
Приклад обробника
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 або моніторингу частоти переадресацій.
Відкривайте дзвінки в інструменті QA для перевірки людиною або запускайте їх через власну модель оцінювання.
Залучайте колегу-людину у разі переадресації / збою.