Перейти к содержанию
Innovialabs

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

Продвинутое3 мин

Коротко

Надежный обработчик вебхуков проверяет подпись, сохраняет каждое событие по уникальному ID, меняет статус заказа только по допустимым переходам и регулярно сверяется с API провайдера.

Главное правило

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

  • проверяемым — принимать только запросы с верной подписью;
  • идемпотентным — повторная обработка того же события ничего не меняет;
  • устойчивым к порядку — старое событие не перезаписывает более новый статус;
  • сверяемым — состояние заказа можно проверить по API провайдера.

Шаг 1. Проверка подписи

Провайдеры обычно подписывают вебхук: HMAC от тела запроса с общим секретом, подпись в заголовке или параметрах. Конкретная схема у каждого своя (Stripe, Payme, Click и другие описывают ее в документации), но принципы общие:

  • проверяйте подпись по сырому телу запроса, до парсинга JSON;
  • сравнивайте подписи функцией с постоянным временем;
  • отклоняйте слишком старые запросы, если провайдер передает метку времени;
  • храните секрет в переменных окружения, а не в коде.
import crypto from "node:crypto";

export function isValidSignature(rawBody: string, signature: string, secret: string) {
  const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Шаг 2. Идемпотентность через таблицу событий

У каждого события есть уникальный идентификатор. Сохраняйте его в таблицу с уникальным индексом до бизнес-логики. Если вставка не удалась из-за дубля — событие уже обработано, отвечайте успехом.

CREATE TABLE payment_events (
  provider     text NOT NULL,
  event_id     text NOT NULL,
  payload      jsonb NOT NULL,
  processed_at timestamptz,
  PRIMARY KEY (provider, event_id)
);

Важно: вставка события и изменение заказа должны идти в одной транзакции. Иначе можно записать событие, упасть и больше никогда не обновить заказ.

Шаг 3. Быстрый ответ и очередь

Провайдер ждет ответа ограниченное время. Схема, которая работает под нагрузкой:

  1. Проверить подпись.
  2. Сохранить событие.
  3. Сразу вернуть успешный HTTP-ответ.
  4. Обработать событие в фоновой очереди с повторами.

Не отправляйте письма, не вызывайте CRM и не формируйте документы синхронно в обработчике — любая медленная операция вызовет таймаут и повторную отправку.

Шаг 4. События не по порядку

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

Текущий статусДопустимые переходы
pendingpaid, failed, cancelled
paidrefunded, partially_refunded
failedpaid (повторная попытка)
refunded—

Если событие предлагает недопустимый переход (например, из refunded обратно в paid), не применяйте его, а запишите в лог для разбора. Дополнительно можно сравнивать время события провайдера с временем последнего изменения заказа.

Шаг 5. Сверка и самовосстановление

Вебхук может не прийти совсем: сбой сети, ошибка на вашей стороне, неверный URL после деплоя. Поэтому нужна сверка:

  • периодическая задача находит заказы, которые долго висят в pending;
  • для них запрашивается статус платежа через API провайдера;
  • расхождения исправляются автоматически или попадают в очередь на ручную проверку.

Так вебхук становится быстрым путем обновления, а не единственным источником истины.

Частые ошибки

  • Парсинг JSON до проверки подписи, из-за чего подпись не совпадает.
  • Ответ ошибкой на дубль — провайдер будет повторять отправку снова и снова.
  • Доверие сумме из вебхука без сверки с суммой заказа.
  • Отсутствие логов с сырым телом запроса — потом невозможно разобрать инцидент.
  • Тестирование только «счастливого пути» без дублей и обратного порядка.

Частые вопросы

Успешный (обычно 200). Событие уже обработано, и ошибка лишь заставит провайдера продолжать повторы. Точные требования к ответу смотрите в документации конкретного провайдера.

Поможем с этим

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

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.