ThunderPhone 2.0 уже доступен.Самостоятельное подключение — от 2 центов/мин.Читать анонс

Webhooks

Обзор вебхуков

Как ThunderPhone доставляет события в реальном времени, как проверять подписи и чем отличаются устаревшая и основанная на эндпоинтах модели доставки.

ThunderPhone отправляет HTTP-запросы POST на ваш сервер, когда во время звонка происходят события — начинается входящий звонок, завершается звонок, завершается запуск оценки, срабатывает оповещение и так далее. Есть две модели доставки:

Все десять типов событий из каталога событий доставляются через конечные точки вебхуков. Шесть событий жизненного цикла звонка (telephony.incoming, telephony.complete, telephony.tool, web.incoming, web.complete, web.tool) также отправляются в устаревший вебхук с одним URL — если у вас есть и устаревший URL, и подходящая конечная точка, вы получите событие по обоим путям. Блокирующее поведение ( обмен конфигурацией telephony.incoming / web.incoming и отправка вызовов инструментов в режиме вебхука) доступно только по устаревшему пути; каждая доставка в конечную точку — это уведомление без ожидания ответа.

Формат полезной нагрузки

Доставка в конечную точку представляет собой объект JSON с data, event_id и type:

{
  "data": {
    "call_id": 987654321,
    "from_number": "+14155550199",
    "to_number": "+15551234567"
  },
  "event_id": "3f6b2ad0-1c9e-4a57-9f2b-8f6f0f9d2f11",
  "type": "telephony.incoming"
}

event_id уникален для каждого отправленного события. Он остаётся одинаковым при повторных попытках и для каждой конечной точки, получающей событие — используйте его для дедупликации.

Устаревший вебхук с одним URL отправляет те же type и data, но без event_id:

{
  "type": "telephony.incoming",
  "data": { "call_id": 987654321, "from_number": "+14155550199", "to_number": "+15551234567" }
}

При передаче каждый объект сериализуется канонически — ключи сортируются по алфавиту, без пробелов, в UTF-8. Примеры с форматированием в этой документации приведены только для удобства чтения.

Полный список типов событий и полей полезной нагрузки см. в каталоге событий.

Проверка подписи

Каждый запрос содержит подпись HMAC-SHA256 для исходного тела запроса в заголовке X-ThunderPhone-Signature. Ключом подписи служит secret конечной точки (или secret вебхука на уровне вашей организации для устаревших доставок).

Шаги

  1. Прочитайте исходное тело запроса до любого разбора.
  2. Вычислите hmac_sha256(secret, body).hexdigest().
  3. Сравните результат с заголовком X-ThunderPhone-Signature за постоянное время.

Мы подписываем в точности те байты, которые передаём, и эти байты представляют собой каноническую сериализацию JSON (отсортированные ключи, компактные разделители). Поэтому проверка по исходному телу всегда работает — а если ваш фреймворк предоставляет только разобранный JSON, его повторная сериализация с отсортированными ключами и компактными разделителями создаёт идентичные байты. Оба способа описаны в руководстве по проверке.

Python
import hmac
import hashlib
 
def verify_signature(body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode("utf-8"),
        body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature or "")
 
# Example Flask handler
from flask import Flask, request, abort
app = Flask(__name__)
 
@app.post("/thunderphone-webhook")
def handle():
    body = request.get_data()
    sig = request.headers.get("X-ThunderPhone-Signature", "")
    if not verify_signature(body, sig, WEBHOOK_SECRET):
        abort(401)
    event = request.get_json()
    # dispatch on event["type"] …
    return "", 204
Node.js (Express)
import crypto from "node:crypto";
import express from "express";
 
function verifySignature(body, signature, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(body)
    .digest("hex");
  if (!signature || expected.length !== signature.length) return false;
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature),
  );
}
 
const app = express();
app.post(
  "/thunderphone-webhook",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const sig = req.header("X-ThunderPhone-Signature") || "";
    if (!verifySignature(req.body, sig, process.env.WEBHOOK_SECRET)) {
      return res.sendStatus(401);
    }
    const event = JSON.parse(req.body.toString("utf8"));
    // dispatch on event.type …
    res.sendStatus(204);
  },
);

Семантика доставки

Эта семантика применяется к доставкам в конечные точки. Устаревший вебхук с одним URL выполняет одну синхронную попытку без повторов.

Повторы

Каждое событие немедленно отправляется один раз. Любой ответ 2xx подтверждает доставку. При любом другом результате (не-2xx, ошибка подключения, тайм-аут) выполняются повторы через 1 мин, 5 мин, 30 мин, 2 ч, 6 ч, 12 ч и 24 ч после первой попытки — всего 8 попыток в течение 24 часов. Если все попытки завершатся ошибкой, доставка прекращается, а конечная точка помечается как status="failing" в разделе конечных точек вебхуков. Возвращайте 2xx, как только полезная нагрузка будет надёжно принята; обрабатывайте её асинхронно.

Порядок

Порядок доставки обеспечивается по мере возможности. На практике мы доставляем события в порядке их отправки, но при ошибках повторы могут изменить порядок. Всегда удаляйте дубликаты и сверяйте данные по call_id / идентификатору объекта.

Дубликаты

Доставка выполняется как минимум один раз: повтор после ответа, который мы не получили, может создать дубликат события. Каждый повтор содержит тот же event_id, поэтому сохраняйте обработанные идентификаторы и пропускайте повторы. event_id также используется всеми конечными точками — две конечные точки, подписанные на одно событие, получают одинаковый event_id.

Тайм-ауты

Для доставки в конечную точку действует тайм-аут 30 с на каждую попытку. В устаревшем пути блокирующие запросы, определяющие поведение активного звонка — обмен конфигурацией telephony.incoming / web.incoming — завершаются по тайм-ауту через 10 с, но медленный ответ задерживает приём звонка, поэтому старайтесь отвечать в течение нескольких секунд. Вызов инструментов в режиме вебхуков по умолчанию допускает 20 с, а объявления инструментов могут задавать timeout верхнего уровня.

Исходящие IP-адреса

Исходящие вебхуки отправляются из диапазона облачных IP-адресов ThunderPhone. Если вашему межсетевому экрану требуется список разрешённых адресов, обратитесь в поддержку, и мы предоставим актуальные диапазоны.

Выбор между устаревшими вебхуками и вебхуками на основе конечных точек

ВозможностьУстаревший (/v1/webhook)Конечные точки (/v1/developer/webhook-endpoints)
Количество URL1 на организациюНесколько на организацию
Охват событийТолько telephony.* / web.*Все 10 типов событий
Фильтр событийДля каждой конечной точки
ПовторыНет8 попыток в течение 24 ч
Конвертtype + datatype + data + event_id
Ротация секретаЗаменяет единственный секретСекрет для каждой конечной точки
Отключение без удаленияstatus=disabled
Видимость статусаactive / disabled / failing
Блокирующий обмен конфигурациейДа (telephony.incoming / web.incoming)Никогда — только уведомления
Лучше всего подходит дляДинамической конфигурации звонковОбработки событий в продакшене

Новые интеграции должны получать события через вебхуки на основе конечных точек. Сохраняйте (или добавляйте) устаревший URL, только если динамически настраиваете звонки в момент приёма или используете вызов инструментов в режиме вебхуков — эти обмены запросами и ответами выполняются только через устаревший путь.


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