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_idintegerНеизменно для каждого события этого вызова
agent_idinteger | nullАгент, обработавший вызов, если он был назначен
agent_namestring | nullАгент, обработавший вызов, если он был назначен
directionstringinbound, outbound, web, test. Исторические полезные нагрузки могут содержать устаревшие значения mic или widget
from_number, to_numberstringE.164. Для веб- и тестовых вызовов from_number имеет буквальное значение "web"
origin_domainstringТолько для веб- и тестовых вызовов — источник страницы, на которой размещён виджет (пусто для сеансов mic)
start_time, end_timetimestampISO 8601 UTC
duration_secondsinteger | nullВычисляется по времени начала и окончания
statusstringcompleted или failed
end_reasonstringСм. таблицу ниже
product, voicestringКонфигурация агента, действовавшая во время вызова
variablesobjectВходные переменные, зафиксированные при начале вызова
unresolved_variablesarrayИмена переменных, на которые ссылается конфигурация вызова, но которые не были переданы при начале вызова
transfer_numberstring | nullУстанавливается при переводе вызова
recording_urlstring | nullПодписанный URL с ограниченным сроком действия; скачайте его без промедления. null, если запись недоступна
recording_stopped_attimestamp | nullМомент остановки записи до завершения вызова; после этого момента запись ничего не содержит. null, если запись велась до конца вызова или вызов не записывался
recording_stopped_reasonstring | nullПричина досрочной остановки записи. warm_transfer: запись останавливается непосредственно перед тем, как тёплый перевод вызывает получателя, и остаётся выключенной, даже если перевод не удался и звонящий вернулся к агенту. Считайте другие значения информационными; могут быть добавлены новые. null, если recording_stopped_at имеет значение null
billable_minutesnumberТарифицируемые минуты, округлённые до ближайшей четверти минуты (интервалы по 15 секунд, минимум 0.25). Вызовы, сразу перенаправленные на голосовую почту, по-прежнему указывают здесь фактическое количество тарифицируемых минут, но плата ограничена одной минутой по тарифу плана.
billing_total_centsintegerЦенты USD
transcriptsarrayЗаписи расшифровки для каждого хода; могут быть пустыми, если расшифровка недоступна
summarystring | nullИтог вызова на естественном языке (от двух до четырёх предложений прозы, не более 2 000 символов, на языке вызова: основном языке агента, если вызов не переопределил его), когда у агента включён параметр Создавать итог вызова в разделе Данные для сбора конструктора агента (call_summary_enabled у Агента) и создание выполнено успешно; в остальных случаях null, в том числе для вызовов, слишком коротких для создания итога, вызовов без расшифровки и агентов с отключённой записью. Итог создаётся на том же этапе после вызова, что и извлечение данных, поэтому параметр Ожидать извлечённые данные перед отправкой вебхука о завершении вызова (extraction_blocks_completion) определяет, ожидает ли это событие его создания: в режиме ожидания итог готов до отправки этого события (самому итогу отводится не более 30 секунд из 75-секундного бюджета этапа); если событие не ожидает (never или перевод при except_transfers), это поле имеет значение null, а итог не отправляется повторно в call.data_extracted, поэтому прочитайте его позже из summary в объекте вызова. Также доступно на странице вызова в панели управления
extracted_dataobject | nullСтруктурированный результат извлечения с полями status, fields, evidence, verification, field_reasons и schema_version. Каждое поле, отличное от null, содержит точную структурно проверенную цитату (не более 1 000 символов), а также роль говорящего и индекс хода; более длинные цитаты, возвращённые моделью, отклоняются, а не обрезаются. Доказательство имеет значение null, если поле имеет значение null. verified означает, что каждый кандидат получил ровно один допустимый независимый вердикт. unavailable также охватывает некорректный или частичный вывод верификатора; допустимые частичные вердикты по-прежнему применяются, а кандидаты без одного допустимого вердикта обнуляются. status имеет значение completed, failed, exhausted, skipped или skipped_recording_disabled; null, если у агента не было полей извлечения
steps_tracearrayТолько для вызовов, агент которых имеет этапы или передачи. Порядок, в котором вызов проходил через них: каждая запись содержит t_ms (миллисекунды с начала вызова), agent (ключ агента, a плюс его ID), step (ID этапа или null при передаче агенту без этапов) и via (start, model, condition, on_enter или handoff). Содержит только ID и время, поэтому сохраняется даже при отключённой записи
collected_valuesobjectТолько для вызовов, агент которых имеет этапы или передачи. Значения, собранные агентом на его этапах, с ключами по имени поля сбора, а также <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"] }
    }
  }
]
ПолеТипОписание
rolestringuser, model, tool или system
content_typestringtext/plain для речи; application/json для вызовов инструментов, результатов инструментов и системных событий
contentstring | objectТекст речи или структурированный объект, показанный выше. Вызовы инструментов: {"tool_call": name, "arguments": {…}}. Результаты инструментов: {"tool_name": name, "response": {…}}
start_ms, end_msintegerСмещения от начала звонка в мс. Присутствуют, когда известно время аудио
ttfa_msintegerВремя до первого аудио для хода model, если оно измерено
audio_url, audio_urlsstring / arrayВременные подписанные URL-адреса аудио для хода, если запись ведётся для каждого хода

Для получения полностью структурированной истории ходов (с маркерами прерываний, запросами подтверждения и исходными позициями) используйте GET /v1/calls/{call_id}/history.

Отличия устаревшей полезной нагрузки

Устаревшая обёртка вебхука с одним URL имеет вид {"type": "telephony.complete" | "web.complete", "data": {…}}, содержит без event_id, а её data отличается от полезной нагрузки эндпоинта:

Устаревшая полезная нагрузка завершения также содержит agent_id и agent_name.

  • Массив ходов находится в history, а не в transcripts (та же схема ходов, что и выше).
  • Набор полей представляет собой исходный отчёт об окончании звонка и может включать дополнительные внутренние поля помимо приведённых в таблице — считайте неизвестные поля информационными.
  • Веб-звонки (direction: "web") не содержат from_number / to_number и добавляют origin_domain.
  • Тестовые звонки с микрофона в конструкторе передаются как 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 для проверки человеком или запускайте их через собственную модель оценки.

Уведомления

Уведомляйте коллегу при переводе или сбое.


Связанные материалы