Webhook, polling или симулятор: какой инструмент на каком этапе разработки
TL;DR: на каждой стадии разработки чат-интеграции уместен свой механизм получения событий. Симулятор — для локальной отладки и разбора payload; polling через cursor — для прототипов и сценариев без публичного endpoint'а. Webhook с HMAC, timestamp-окном и idempotency — для прода. Ниже — код для каждого режима, таблица сравнения и критерии, когда пора переключаться.
Коротко
- Три инструмента, три задачи. Симулятор отвечает на вопрос «что вообще приходит», polling даёт живые события без публичного endpoint'а, webhook — минимальную задержку в проде. Они не заменяют друг друга, а закрывают разные стадии.
- Переключение — по замеру, не по вкусу. С симулятора на polling переходят, когда обработчик разбирает payload; с polling на webhook — когда задержка стала критична (< 5–10 с) или число событий упёрлось в rate-limit (обычно 100+ в час).
- Прод-webhook = три проверки. HMAC подтверждает отправителя, timestamp в подписи (±5 минут) закрывает replay через час, idempotency по
event.idчерезSET NX EX— дубли и replay внутри окна. Все три обязательны и именно в этом порядке.
В разработке любой чат-интеграции есть три стадии, и для каждой подходит свой механизм получения событий. Если выбрать неправильно — либо тестирование становится медленным (часы на каждое изменение), либо прод не выдерживает нагрузку, либо обработчик ловит replay-атаку через неделю после релиза. Ниже — разбор всех трёх инструментов: что делает каждый, какой код за ним стоит и в какой момент пора переключаться на следующий.
| Стадия | Инструмент | Зачем |
|---|---|---|
| Локальная разработка | Webhook-симулятор | Понять структуру payload, проверить headers, без подключения канала |
| Прототип / CI | Polling через cursor | Получать события без публичного endpoint'а |
| Прод | Webhook + HMAC + replay | Минимальная задержка, гарантия аутентичности |
Симулятор: увидеть структуру payload до подключения канала
Типичный сценарий: разработчик пишет обработчик, который должен принимать webhook — поступление сообщения из мессенджера, оплату, изменение статуса заказа. Чтобы убедиться, что обработчик валидирует подпись, разбирает JSON, кладёт событие в очередь и отвечает 200 OK за допустимое время, нужен реальный входящий запрос. И вот тут начинается лишняя работа: завести аккаунт у внешнего сервиса, получить токен, настроить бота, попросить кого-то отправить сообщение, дождаться доставки. Часы вместо минут.
При этом сам обработчик к каналу не привязан. Ему нужен лишь корректно сформированный HTTP-запрос с теми же headers, тем же payload и той же подписью, что отправил бы прод. Никакого специального состояния «канал подключён» внутри обработчика нет — значит, весь блок подключения канала на этапе разработки лишний. Решается это инструментом, который в индустрии устоялся как webhook-инспектор.
Webhook-инспектор — это сервис, который выдаёт временный публичный URL и показывает всё, что в этот URL приходит: метод, путь, query-параметры, headers, body, время прихода. В BOTIX он живёт на отдельном поддомене webhook.botix.pro. Заходим, нажимаем «Create new URL» — получаем что-то вроде https://webhook.botix.pro/r/8f3a1c9b. TTL по умолчанию 7 дней, после чего bin и все накопленные запросы удаляются автоматически.
# Создаём bin вручную через UI или используем готовый URL
# Шлём любой источник на этот URL
curl -X POST https://webhook.botix.pro/r/8f3a1c9b \
-H "Content-Type: application/json" \
-H "X-BOTIX-Signature: sha256=demo" \
-d '{"event":"message.created","contact_id":42,"text":"привет"}'Открываем https://webhook.botix.pro/r/8f3a1c9b в браузере — видим запрос: метод, путь, query, headers, body, время прихода. Любой запрос можно скопировать как curl и воспроизводить на своём localhost сколько угодно раз.
С точки зрения архитектуры это маленький сервис: один nginx server-блок, два эндпоинта (создание bin'а и приём входящего запроса), две таблицы в БД (bins и bin_requests с CASCADE на удаление). Связи с основной платформой по бизнес-логике нет — bin не знает, к какому проекту привязан, и не требует авторизации. Сделано намеренно, чтобы инспектор оставался изолированным песочничным инструментом, а его доступность не зависела от состояния остальной платформы.
Когда использовать. На этапе «я ещё не написал обработчик, но мне нужно понять, что вообще приходит». Особенно полезно с новым типом события: открыл bin, дал URL платформе, увидел структуру за 30 секунд.
Когда не использовать. На проде. Инспектор не пишет никуда, кроме своей памяти, и не вызывает бизнес-логику. Это инструмент инспекции, не приёмник. Вторую половину задачи — отладку самого кода обработчика — он тоже не закрывает: для детерминированного входа нужна кнопка «Тестовая отправка» в кабинете на app.botix.pro/developers, которая шлёт payload известной структуры с настоящей подписью на указанный URL. Про полную проверку цепочки с боевой подписью — в разборе testing strategy для webhook: симулятор и Verifier.
Polling vs webhook: критерии выбора
Когда нужно реагировать на события внешнего сервиса, есть две принципиально разные опции. Либо самому периодически дёргать API и спрашивать «что нового» (polling), либо подписаться на webhook и получать события push'ем. Внешне разница небольшая — суть та же, получить новое событие. Но в проде это разная нагрузка, разная задержка и разные требования к своей инфраструктуре.
Polling — это запрос вида «дай мне всё, что появилось после такой-то отметки». В BOTIX эндпоинты GET /public/v1/chats, /messages, /contacts поддерживают cursor и возвращают meta.next_cursor:
def poll_messages():
cursor = None
while True:
params = {"limit": 100}
if cursor: params["cursor"] = cursor
r = requests.get(f"{API}/messages", headers=HEADERS, params=params)
data = r.json()
for msg in data["data"]:
process_message(msg)
cursor = data["meta"]["next_cursor"]
if not cursor:
time.sleep(30)Простота — главное достоинство. Никакого публичного endpoint'а, никакого HTTPS-сертификата, никакой проверки подписи. Воркер polling'а — маленький while-loop с requests.get и cursor'ом: на localhost, в Docker, в Lambda запустится без настройки сети. Используем для локальной разработки без туннеля, CI-окружения, cron-job раз в час, прототипа на неделю-две.
Недостаток — обратная сторона простоты. Период опроса определяет лаг. Раз в секунду — лаг до 1 секунды, но 86 400 запросов в сутки на ключ. Раз в минуту — лаг до 60 секунд, для интерактивного бота уже неприемлемо. Удержать одновременно малый лаг и запас по rate-limit не выйдет: либо ускорять опрос и платить лимитом, либо терпеть задержку.
Webhook — HTTP-запрос, который платформа сама отправляет на endpoint клиента в момент события. Задержка минимальная (десятки-сотни миллисекунд), никаких пустых запросов. Цена — инфраструктура: публичный endpoint, HTTPS с валидным сертификатом, обработка повторных доставок, проверка подписи на каждом запросе, тайм-аут ответа в пределах нескольких секунд. Базовый обработчик с тремя слоями защиты:
SECRET = XXXXXXXX["BOTIX_WEBHOOK_SECRET"].encode()
r = redis.Redis()
def verify(raw_body, sig, ts):
if abs(time.time() - int(ts)) > 300: # окно ±5 минут
return False
signed = f"{ts}.".encode() + raw_body
expected = hmac.new(SECRET, signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig.replace("sha256=", ""))
@app.route("/webhook", methods=["POST"])
def webhook():
raw = request.get_data()
if not verify(raw, request.headers.get("X-BOTIX-Signature", ""),
request.headers.get("X-BOTIX-Timestamp", "0")):
return "", 401
payload = json.loads(raw)
key = f"webhook:processed:{payload['event_id']}"
if not r.set(key, "1", ex=900, nx=True):
return "", 200 # дубль
r.lpush("webhook:incoming", raw) # асинхронная обработка
return "", 200Разбор трёх слоёв этого обработчика — в следующем разделе про replay-защиту. Здесь важнее критерий выбора между двумя механизмами:
| Параметр | Симулятор | Polling | Webhook |
|---|---|---|---|
| Публичный endpoint нужен | Нет | Нет | Да |
| HTTPS-сертификат | Не нужен | Не нужен | Обязателен |
| Проверка подписи | Не нужна | Не нужна | Обязательна |
| Задержка | Real-time view | Период опроса | < 1 сек |
| Нагрузка на платформу | Минимальная | Высокая (опросы) | Минимальная |
| Подходит для прода | Нет | Только cron-сценарии | Да |
| Время на настройку | 30 секунд | 5 минут | 30-60 минут |
| Что нужно знать | UI инспектора | cursor-пагинация | HMAC, replay, idempotency |
Простой алгоритм для типового сценария. Есть стабильный публичный endpoint и нужна задержка меньше 5 секунд — webhook, без раздумий. Разработка на локальной машине и публичный endpoint поднять сложно — polling через cursor с периодом 30-60 секунд, а на webhook переходим при выходе в прод. Интеграция запускается из cron-job раз в час — polling, webhook здесь бессмысленен. Правила выбора и серверный backoff разобраны отдельно — в polling или webhook: 5 правил выбора и backoff.
И отдельный пункт: никогда не сравнивайте polling с webhook'ом по «удобству на старте», не сделав замер ожидаемого трафика и допустимой задержки. Polling часто кажется проще на MVP, но при масштабировании первым ломается. Webhook сложнее настроить, зато дальше работает сам и не требует пересмотра при росте нагрузки.
Replay-защита webhook: почему одной подписи HMAC мало
Подпись подтверждает, что сообщение пришло от вас. Она не подтверждает, что оно пришло *сейчас* и *в первый раз*. HMAC отвечает только на вопрос «принадлежит ли это сообщение владельцу секрета». Он не отвечает на два других, не менее важных в production: «пришло сейчас или его подобрали из старого лога» и «я ещё не обрабатывал это событие».
Если игнорировать эти два вопроса, появляется replay-атака. Прокси, через который однажды прошёл webhook, сохранил тело и заголовки. Тело подписано, подпись валидна, никто её не подделывает. Злоумышленник берёт уже подписанный запрос и отправляет повторно. Подпись на стороне получателя сходится, бизнес-логика выполняется второй раз. Если webhook обрабатывает оплату, начисление бонусов или изменение прав — последствия серьёзные. Именно поэтому обработчик выше строится на трёх слоях.
Слой 1 — HMAC-SHA256 от raw body. Подтверждает отправителя. Важнейший нюанс — raw body: если фреймворк парсит JSON и сериализует обратно, подпись не сойдётся. В Express нужен bodyParser.raw, во Flask — request.get_data(), в Laravel — $request->getContent().
Слой 2 — timestamp в подписи, окно ±5 минут. Timestamp выхода события включается в строку, по которой считается HMAC:
signed_payload = timestamp + "." + raw_body
expected_sig = hmac_sha256(secret, signed_payload)Если модуль разницы между timestamp и текущим временем больше окна — webhook отбрасывается без обработки. В BOTIX окно ±5 минут: хватает на сетевые задержки, retry-каскады и расхождения часов. Окно симметрично — timestamp из будущего так же опасен, как из прошлого. И timestamp должен быть *частью подписи*: если передавать его отдельным заголовком вне HMAC, злоумышленник просто подменит его в перехваченном запросе на свежий, и HMAC сойдётся.
Если не хочется писать верификацию руками, вспомогательная функция в SDK делает обе проверки за один вызов и заодно закрывает compare_digest вместо ==:
from botix.webhooks import verify
is_valid = verify(
payload=raw_body,
signature=request.headers["X-Botix-Signature"],
timestamp=request.headers["X-Botix-Timestamp"],
secret=XXXXXXXX["BOTIX_WEBHOOK_SECRET"],
tolerance=300, # секунд
)Слой 3 — idempotency по event.id. Timestamp-окно отрезает replay через час или сутки, но внутри пяти минут он остаётся возможным. Здесь получатель должен помнить, какие события уже обрабатывал. Выдумывать nonce не нужно — у любого осмысленного события он уже есть: в BOTIX каждое событие содержит event.id в формате evt_ + 24 символа, уникальный во всей платформе и не повторяющийся при retry. В Redis это одна строчка:
key = f"webhook:processed:{event_id}"
if not r.set(key, "1", ex=900, nx=True):
return 200 # уже обрабатывали
process_event(payload)Команда SET … NX EX 900 атомарна: конкурентные запросы с одним event.id гарантированно увидят, что ключ занят, и пройдут мимо. Достаточно хранить идентификаторы за интервал в полтора-два раза больше окна свежести — десять-пятнадцать минут. Это не журнал событий, а короткий кеш для отбрасывания дубликатов.
Типичных провалов три. Забыли про timestamp вообще. Проверяют timestamp, но не включают его в подписываемую строку. Проверяют event.id через SELECT, потом INSERT без транзакции и без ON CONFLICT — и при двух одновременных webhook'ах обрабатывают дважды. Защита от replay — это три независимые проверки в строгом порядке: дешёвая проверка timestamp перед дорогой проверкой подписи, и обе — перед обращением в кеш обработанных событий. Подпись не заменяет timestamp, timestamp не заменяет idempotency. Полный разбор трёх ошибок — в replay-защита webhook: timestamp + nonce.
Когда переключаться между инструментами
С симулятора на polling. Когда обработчик научился разбирать payload и нужны живые события для интеграционных тестов. Полминуты — нормальное окно опроса для разработки.
С polling на webhook. Когда либо задержка стала критична (бот должен отвечать в пределах нескольких секунд, а не минуты), либо число событий выросло и polling упирается в rate-limit. Граница — обычно 100+ событий в час либо требование к отклику меньше 10 секунд.
С webhook обратно на polling. Только в инцидентах — например, обработчик упал и нужно догнать пропущенное за час через GET-запросы. После починки возвращаемся к webhook.
Гибридный паттерн для зрелых интеграций
Зрелые интеграции используют комбинацию. Симулятор — на этапе разработки и тестов. Webhook — основной канал доставки. Polling — резерв: periodic reconciliation раз в час, когда клиент опрашивает API через cursor и сверяет локальное состояние с состоянием платформы. Если что-то разошлось (webhook потерялся, обработчик ответил ошибкой) — reconciliation это исправит. Webhook закрывает 99% случаев, reconciliation страхует остаток. Паттерн особенно полезен, когда последствия пропущенного события дорогие — например, не доставили триггерное сообщение по платёжному статусу.
Что в BOTIX
Webhook через webhook.botix.pro — встроенный инспектор для разбора структуры. Доставка событий идёт через WebhookDispatcher с очередью в БД и retry-расписанием 30 секунд / 5 минут / 30 минут / 2 часа / 6 часов; после 5 неудач попытка помечается failed и видна в delivery dashboard. Подпись — в заголовке X-Botix-Signature как sha256=… от тела запроса. Регистрация получателя — POST /public/v1/webhooks либо в кабинете на app.botix.pro/developers.
Все три SDK идут с готовым Webhook::verify() — composer require botix-pro/sdk, pip install botix, npm install @botix/sdk — и делают timing-safe сравнение автоматически. Cursor-пагинация — на всех GET эндпоинтах (cursor + meta.next_cursor), в документации developers.botix.pro/best-practices polling явно отмечен как fallback, не как основной способ. Кнопка «Тестовая отправка» в кабинете отправляет реальный webhook с настоящей подписью на указанный URL — проверка полной цепочки в одно действие.
Итог
Три инструмента закрывают три разные задачи и не заменяют друг друга. Симулятор — для разбора структуры payload без подключения канала. Polling через cursor — для прототипов и cron-сценариев без публичного endpoint'а. Webhook с HMAC, timestamp-проверкой и idempotency — для прода. Выбор между polling и webhook делается не по «удобству на старте», а по замеру ожидаемого трафика и допустимой задержки: polling проще на MVP, но первым ломается при масштабировании; webhook дороже в настройке, но дальше работает сам.
Попробуйте API BOTIX
Получите ключ, отправьте первый запрос и поймайте webhook — прямо в браузере, без боевого канала.
Попробовать API бесплатноЧитайте также
Webhook в проде: HMAC, симулятор и выбор с polling
Webhook и polling: retry-политика, dead-letter и backoff по 429
Тест безопасности webhook: симулятор и репетиция replay
Production-ready POST: idempotency, retry и rate-limit
Multi-tenant, polling vs webhook и OAuth для агентства
