Что это решает
Опрос чужого 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, после чего по журналу поставщика видно, действительно ли пришли повторные доставки и с какими интервалами. Это единственный способ узнать реальную политику ретраев, а не ту, что написана в документации.
Проверка скорости ответа: обработка искусственно замедляется, а время ответа должно остаться прежним. Если выросло — обработка выполняется внутри запроса, и это чинится до запуска, а не после первого всплеска нагрузки.
Сверка запускается по расписанию и пишет результат в лог даже при нуле расхождений: молчащая сверка неотличима от сломанной.
Срок действия сертификата на домене приёмника ставится в мониторинг вместе с остальными проверками доступности эндпоинта снаружи.
