Обзор вебхуков
Как ThunderPhone доставляет события в реальном времени, как проверять подписи и чем отличаются устаревшая и основанная на эндпоинтах модели доставки.
ThunderPhone отправляет HTTP-запросы POST на ваш сервер, когда во время звонка
происходят события — начинается входящий звонок, завершается звонок, завершается
запуск оценки, срабатывает оповещение и так далее. Есть две модели
доставки:
Несколько URL-адресов, секреты для каждой конечной точки, фильтры событий для каждой конечной точки
и автоматические повторные попытки.
Управляйте через GET/POST/PATCH/DELETE /v1/developer/webhook-endpoints.
Один URL для каждой организации. Передаёт события жизненного цикла звонка, включая
блокирующие обмены конфигурацией. Управляется через GET/PUT /v1/webhook.
Все десять типов событий из каталога событий
доставляются через конечные точки вебхуков. Шесть событий жизненного цикла звонка
(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 вебхука на уровне вашей организации для устаревших
доставок).
Шаги
- Прочитайте исходное тело запроса до любого разбора.
- Вычислите
hmac_sha256(secret, body).hexdigest(). - Сравните результат с заголовком
X-ThunderPhone-Signatureза постоянное время.
Мы подписываем в точности те байты, которые передаём, и эти байты представляют собой каноническую сериализацию JSON (отсортированные ключи, компактные разделители). Поэтому проверка по исходному телу всегда работает — а если ваш фреймворк предоставляет только разобранный JSON, его повторная сериализация с отсортированными ключами и компактными разделителями создаёт идентичные байты. Оба способа описаны в руководстве по проверке.
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 "", 204import 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) |
|---|---|---|
| Количество URL | 1 на организацию | Несколько на организацию |
| Охват событий | Только telephony.* / web.* | Все 10 типов событий |
| Фильтр событий | — | Для каждой конечной точки |
| Повторы | Нет | 8 попыток в течение 24 ч |
| Конверт | type + data | type + data + event_id |
| Ротация секрета | Заменяет единственный секрет | Секрет для каждой конечной точки |
| Отключение без удаления | — | status=disabled |
| Видимость статуса | — | active / disabled / failing |
| Блокирующий обмен конфигурацией | Да (telephony.incoming / web.incoming) | Никогда — только уведомления |
| Лучше всего подходит для | Динамической конфигурации звонков | Обработки событий в продакшене |
Новые интеграции должны получать события через вебхуки на основе конечных точек. Сохраняйте (или добавляйте) устаревший URL, только если динамически настраиваете звонки в момент приёма или используете вызов инструментов в режиме вебхуков — эти обмены запросами и ответами выполняются только через устаревший путь.
Связанные материалы
Все типы событий и их полезные нагрузки.
Управление несколькими конечными точками, фильтрами событий и секретами.
Блокирующий запрос, на который ваш сервер должен ответить для настройки звонков.
Полезная нагрузка после звонка с расшифровкой, записью и метриками.