Главная/Блог/Вебхуки: подписи, повторы и идемпотентность против потерянных и задвоенных оплат

Вебхуки: подписи, повторы и идемпотентность против потерянных и задвоенных оплат

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

Что такое вебхук и почему события теряются

Вебхук — это обычный HTTP POST-запрос, который провайдер (платёжная система, CRM, служба доставки) отправляет на заранее заданный URL вашего приложения. Внутри — данные события в JSON: идентификатор платежа, сумма, статус. Проблема в том, что доставка не гарантирована: сервер может быть недоступен из-за деплоя, перегрузки, просроченного SSL-сертификата или сетевого сбоя. Добавляются и человеческие факторы: тестовый URL остался в продакшене, файрвол закрыл порт, прокси обрезал запрос. Практика интеграций показывает: значительная часть «пропавших» оплат — это не сбои платёжной системы, а недоступность или ошибка приёмника в момент отправки уведомления.

Проверка подписи: доверяй, но проверяй

Вебхук приходит на публичный URL, а значит, теоретически отправить его может кто угодно. Злоумышленник, узнавший адрес приёмника, сможет «сообщить» вашей системе о фиктивной оплате — и вы отгрузите товар без денег. Поэтому каждый запрос проверяется криптографической подписью: провайдер подписывает тело запроса секретным ключом, а сервер пересчитывает подпись у себя и сравнивает. Например, CloudPayments передаёт HMAC-SHA256-подпись в заголовке, а YooKassa рекомендует не полагаться только на уведомление и перепроверять статус платежа прямым запросом к API. Практические правила: секретный ключ хранится только на сервере, подпись проверяется до любой бизнес-логики, а если провайдер передаёт временную метку — запросы «старше» 5–10 минут отклоняются, это защита от replay-атак с перехваченным пакетом.

Повторные попытки: почему нужно быстро ответить «200 OK»

Провайдеры не бросают вебхук после первой неудачи: Stripe повторяет отправку до 3 суток с нарастающими интервалами, другие платёжные системы и CRM делают от 5 до нескольких десятков попыток в течение суток. Это спасает от потерь, но создаёт главную ловушку. Если обработчик отвечает медленно или с ошибкой — из-за тайм-аута, деплоя или упавшего смежного сервиса, — провайдер пришлёт то же событие снова, и бизнес-логика выполнится повторно. При этом у большинства платёжных систем тайм-аут ожидания ответа — от нескольких секунд до полуминуты: если за это время код успевает создать заказ, списать остатки на складе, отправить письмо и выгрузить данные в 1С, дубли практически гарантированы. Вывод простой: «200 OK» должен уходить мгновенно, а вся тяжёлая работа — выполняться отдельно и асинхронно.

Идемпотентность: одно событие — одно действие

Идемпотентность означает, что повторная обработка того же события не меняет результат: платёж зачтён один раз, сколько бы уведомлений ни пришло. Это не опция, а обязательное требование — провайдеры прямо документируют модель доставки at-least-once, то есть событие может прийти более одного раза. Основа реализации — уникальный идентификатор: у каждого вебхука есть свой ID события, и на уровне базы данных этот ключ должен быть уникальным. Рабочие подходы:
  • уникальный индекс по ID события в таблице обработанных вебхуков — повторная вставка отбрасывается на уровне СУБД;
  • журнал событий: сначала запись о событии с проверкой уникальности, затем бизнес-действие в той же транзакции;
  • идемпотентный ключ при инициализации платежа, который передаётся платёжному API ещё на этапе создания;
  • блокировка по ID платежа (например, SELECT ... FOR UPDATE), чтобы два параллельных уведомления по одному платежу не прошли одновременно.
Отдельно защищайте самые дорогие действия: перевод заказа в отгрузку, начисление бонусов, активацию лицензии — именно их дороже всего оплачивать при дубле.

Очередь задач: приём отделить от обработки

Архитектурное правило, решающее 90% проблем: обработчик вебхука занимается только приёмом, а не бизнес-логикой. Конвейер выглядит так:
  1. получить запрос и проверить подпись;
  2. сохранить «сырой» payload в журнал — это страховка для разбора инцидентов;
  3. проверить уникальность ID события и поставить задачу в очередь (RabbitMQ, Redis, Kafka);
  4. мгновенно ответить «200 OK»;
  5. фоновый воркер достаёт задачу, применяет бизнес-логику и помечает событие обработанным.
Такая схема снимает проблему тайм-аутов, переживает недоступность смежных систем — задача по выгрузке в 1С просто останется в очереди — и даёт естественные точки мониторинга: глубину очереди, число повторов, «отравленные» сообщения.

Порядок событий: уведомления приходят не по очереди

Мало кто учитывает, что вебхуки могут приходить в произвольном порядке: уведомление об отмене иногда обгоняет уведомление об оплате, а повторная отправка старого события приходит позже нового. Если обработчик слепо применяет статус из запроса, заказ может «откатиться» из отгрузки обратно в оплату или наоборот — закрыться раньше времени. Решение — конечный автомат статусов: перед применением события сверяйте его временную метку с текущим состоянием заказа и запрещайте переходы назад по бизнес-процессу, например из «доставляется» в «оплачен». Там, где провайдер передаёт версию или дату изменения, храните её у себя и молча игнорируйте устаревшие события.

Мониторинг и сверка: страховка, которая ловит остатки

Даже идеальный приёмник не даёт стопроцентной гарантии: событие может не дойти из-за сбоя на стороне провайдера или простоя вашего сервера дольше окна повторов. Поэтому нужны два контура защиты. Первый — журналирование всех входящих запросов, включая отбракованные по подписи, плюс алерты на рост ошибок 5xx, повторные доставки одного события и копящуюся очередь. Второй — регулярная сверка с API провайдера: раз в 10–15 минут запрашивайте статусы «висящих» платежей и заказов напрямую и закрывайте расхождения. Сверка превращает потерянный вебхук из потерянной оплаты в запись, отложенную на пару минут.

Чек-лист перед запуском: что проверить у разработчика

Перед запуском приёма платежей задайте подрядчику эти вопросы — каждый пункт занимает у опытного инженера десятки минут, а отсутствие любого из них оборачивается часами разбирательств:
  • подпись каждого вебхука проверяется секретным ключом, и секрет не лежит в коде или публичном репозитории;
  • ответ «200 OK» уходит за секунды, бизнес-логика вынесена в очередь;
  • повторное событие с тем же ID не выполняет действие второй раз — проверено тестом с ручным дублем;
  • статусы заказа меняются только вперёд по бизнес-процессу, устаревшие события игнорируются;
  • все входящие запросы логируются в неизменном виде, по ошибкам 5xx настроены алерты;
  • настроена периодическая сверка статусов платежей через API провайдера;
  • для тестов есть отдельный URL и тестовые ключи, переключение на продакшн — осознанное действие в конфигурации.
Вебхуки — надёжный механизм ровно до тех пор, пока приёмник спроектирован как приёмник, а не как «обычный обработчик формы». Три опоры — проверка подписи, мгновенный ответ с вынесением обработки в очередь и идемпотентность — закрывают и подделки, и потери, и задвоения. Добавьте контроль порядка статусов, мониторинг и регулярную сверку с API — и оплата не потеряется даже при сбоях на обеих сторонах. Если планируете интеграцию с платёжной системой, CRM или 1С, попросите подрядчика описать именно эти сценарии до начала разработки: так вы отличите инженера от исполнителя, который «подключит уведомления за час».

Нужна разработка?

Оставьте заявку — обсудим ваш проект и предложим решение