Справочник14 августа 2026 г.·обновлено 14 августа 2026 г.

Вебхуки: как принимать события и не потерять их

Вебхук доставляет событие за секунды, но гарантирует его не больше одного раза «хотя бы однажды» — с дублями, без порядка и с молчаливой потерей после исчерпания повторов. Разбор приёмника, который это переживает: подпись, журнал сырых событий, очередь и сверка.

Что это решает

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

Как устроено

Подписка

Адрес приёмника регистрируется в кабинете поставщика или его API. Обычно там же выбирается список типов событий и задаётся общий секрет. Хороший тон — свой отдельный путь на каждого поставщика (/hooks/<имя>), потому что формат тела, схема подписи и правила повторов у всех разные, а разбирать это в одном обработчике через набор условий больно.

Доставка

Событие уезжает HTTP-запросом, чаще POST с JSON-телом. В заголовках обычно едут идентификатор события, его тип, подпись и метка времени. Ответ 2xx считается подтверждением. Любой другой код или таймаут запускает повторы по нарастающим интервалам: несколько минут, потом часы, потом отказ. Часть поставщиков после серии неудач просто отключает подписку — и о молчании узнают через неделю.

Что гарантировано и что нет

Практический стандарт доставки — «хотя бы один раз». Из этого следуют два факта, которые определяют весь дизайн приёмника. Дубли нормальны: сеть оборвалась после обработки, но до ответа, и то же событие придёт снова. Порядок не гарантирован: при параллельной доставке «заказ отменён» вполне приходит раньше «заказ создан», особенно после серии повторов. Состояние объекта поэтому вычисляется по данным события и меткам времени, а не по факту прибытия.

Подпись

Подпись — это HMAC-SHA256 от точных байтов тела запроса на общем секрете, иногда с добавленной меткой времени. Три правила, о которые спотыкаются регулярно. Считать надо по сырому телу, прочитанному до разбора: фреймворк, который распарсил JSON и собрал его обратно, меняет порядок ключей и пробелы, и подпись перестаёт сходиться. Сравнивать надо функцией постоянного времени, а не обычным равенством строк. Метка времени проверяется на свежесть, иначе перехваченный кем-то валидный запрос переигрывается сколько угодно раз. Отдельная ловушка — регистр и точный состав параметров в схемах, где подпись считается не по телу, а по перечню полей: лишнее или пропущенное поле даёт ту же ошибку, что и неверный секрет, а сообщение при этом будет про «объект не найден».

Толстое тело или тонкое уведомление

Одни поставщики шлют полный объект, другие — только идентификатор и тип события. Второй вариант («уведомили — сходи забери») надёжнее: он не зависит от порядка доставки, потому что по API всегда придёт актуальное состояние, и не таскает персональные данные лишний раз. Ценой становится дополнительный запрос и зависимость от лимитов API поставщика.

Правильная форма обработчика

Четыре шага, и порядок в них принципиален. Проверить подпись и отбросить чужое. Записать событие целиком в журнал или очередь — сырое тело, заголовки, время получения. Ответить 200. Обработать асинхронно отдельным воркером. Обработка внутри HTTP-запроса упирается в таймаут поставщика (обычно единицы секунд) и превращает любой тормоз внешней системы в лавину повторов.

Идемпотентность обеспечивается уникальным индексом по идентификатору события в журнале. Пришёл дубль — вставка отваливается, обработчик отвечает 200 и не делает ничего. Если поставщик идентификатор не присылает, ключ собирается из бизнес-полей: тип события, объект, метка времени.

Что делать с провалами

Неразобранные события копятся в очереди с числом попыток. После исчерпания попыток они уходят в отдельный отстойник, а не удаляются: почти всегда там лежит либо кривой формат, который никто не ждал, либо баг обработчика, и после исправления пачка переигрывается одной командой. Поверх этого нужна периодическая сверка: раз в сутки список объектов за период тянется из API поставщика и сравнивается с тем, что фактически принято.

Полезные сценарии

  • Платёжный шлюз: подтверждение оплаты, возврат, отказ по подписке — с обязательной проверкой суммы и валюты по своей записи заказа.
  • CI/CD: событие о завершении сборки или мерж-реквесте запускает деплой или уведомление в чат.
  • Мессенджеры и почта: входящие сообщения и статусы доставки писем прилетают событием вместо опроса ящика.
  • CRM и формы: смена стадии сделки или новая заявка мгновенно поднимает задачу у ответственного.
  • Логистика: статусы отправлений от перевозчика ложатся в карточку заказа без ручного трекинга.

Ограничения

Доставка не гарантирована окончательно. Повторы конечны, и после их исчерпания событие исчезает. Без периодической сверки по API дыра остаётся незамеченной, пока клиент не спросит, где его оплаченный заказ.

Простой приёмника прямо конвертируется в потери. Час недоступности переживают те поставщики, у кого повторы растянуты на сутки; те, у кого три попытки за пять минут, не переживают.

Порядка нет. Логика «если пришло событие Б, значит А уже обработано» ломается на первом же ретрае.

Эндпоинт публичный по определению. Он доступен всему интернету, и без проверки подписи и лимита частоты запросов это открытая дверь: чужой POST с правдоподобным телом делает вид, что оплата прошла.

Сертификат домена приёмника — точка тихого отказа. Истёк — поставщик перестаёт доставлять и часто молчит об этом.

Отладка неудобна. Локальная машина недоступна снаружи, поэтому нужен туннель, а повторить конкретное вчерашнее событие можно только если поставщик даёт реплей из кабинета. Пойманное сырое тело стоит сохранять в тесты.

Секрет требует ротации, а ротация — окна, где валидны старый и новый ключ одновременно; в одиночку это не делается ни у одного поставщика без короткого периода двойной проверки.

Ответ 200 означает только «принято». Считать его отчётом об успешной обработке нельзя: сообщение об успехе клиенту отправляется после фактической записи, а не после приёма.

Как проверить результат

Журнал сырых событий с полями «получено», «обработано», «статус», «число попыток» — первый экран любой диагностики. Один запрос к нему отвечает сразу на три вопроса: доходят ли события вообще, обрабатываются ли они и где застряли.

Метрика, которую стоит вывести в мониторинг: количество событий в статусе необработанных старше нескольких минут. Она ловит и упавший воркер, и зависшую внешнюю систему, и новый тип события, который никто не разбирает.

Проверка дубля: то же самое тело отправляется на эндпоинт дважды. В целевой системе должна остаться одна запись, а второй запрос — вернуть 200.

Проверка подписи: тело с испорченной подписью обязано получить отказ, а не тихое 200. Отдельно проверяется мусор — пустое тело, не-JSON, неизвестный тип события: приёмник отвечает контролируемо, а не пятисоткой.

Проверка повторов: обработчик временно возвращает 500, после чего по журналу поставщика видно, действительно ли пришли повторные доставки и с какими интервалами. Это единственный способ узнать реальную политику ретраев, а не ту, что написана в документации.

Проверка скорости ответа: обработка искусственно замедляется, а время ответа должно остаться прежним. Если выросло — обработка выполняется внутри запроса, и это чинится до запуска, а не после первого всплеска нагрузки.

Сверка запускается по расписанию и пишет результат в лог даже при нуле расхождений: молчащая сверка неотличима от сломанной.

Срок действия сертификата на домене приёмника ставится в мониторинг вместе с остальными проверками доступности эндпоинта снаружи.

  • вебхуки
  • интеграции
  • API
  • надёжность