ThunderPhone 2.0 уже доступний.Підключайте самостійно — від 2 ¢/хв.Прочитати анонс

Webhooks

telephony.complete / web.complete

Неблокувальний вебхук, що надсилається після завершення дзвінка, із транскриптом, URL запису та метриками.

Подія завершення спрацьовує після завершення кожного дзвінка — вхідної телефонії, вихідної телефонії, вебдзвінка або тестового дзвінка (сеанс мікрофона в конструкторі). Вона неблокувальна: надішліть будь-яку відповідь 2xx.

Подія доставляється обома шляхами:

Тіло запиту (доставки до ендпойнта)

{
  "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}, щоб повідомити, що ціль переадресації ще не готова. Будь-яка інша відповідь (або відсутність застарілого вебхука) дозволяє переадресації продовжитися. Доставки кінцевої точки для цього ніколи не використовуються.

Приклад обробника

Python (FastAPI)
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}
Node.js (Express)
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 });
  },
);

Поширені сценарії використання

Інтеграція з CRM

Зберігайте транскрипт кожного дзвінка та URL запису разом із даними клієнтів.

Аналітика

Передавайте транскрипти в конвеєр для моделювання тем, вилучення сигналів CSAT або моніторингу частоти переадресацій.

Перевірка якості

Відкривайте дзвінки в інструменті QA для перевірки людиною або запускайте їх через власну модель оцінювання.

Сповіщення

Залучайте колегу-людину у разі переадресації / збою.


Пов’язані матеріали