Документация/Гайды/Вебхуки

Вебхуки

Каталог событий генерируется из кода доставки — таблица не может разойтись с тем, что реально отправляет backend.

Требования к endpoint

  • только публичный HTTPS URL, без учётных данных в самом URL;
  • DNS не должен указывать на private-, loopback- или link-local-адрес;
  • endpoint не должен отвечать редиректом;
  • 2xx возвращается быстро и только после надёжной записи события в очередь или БД; тяжёлая обработка — асинхронно.

Источник доверия — подпись, а не адрес отправителя. Не ограничивайте свой endpoint по IP платформы: адреса доставки не входят в контракт и меняются.

События

payment · payload root: data.payment

payment.createddata.paymentcreatedСессия создана
payment.processingdata.paymentprocessingПровайдер обрабатывает операцию
payment.requires_actiondata.paymentrequires_actionНужен 3-DS или OTP плательщика
payment.authorizeddata.paymentauthorizedСредства захолдированы, не списаны
payment.succeededdata.paymentsucceededОплата завершена, деньги зафиксированы
payment.declineddata.paymentdeclinedОтклонено эмитентом или антифродом
payment.faileddata.paymentfailedТехническая ошибка проведения
payment.canceleddata.paymentcanceledОтменено до списания
payment.expireddata.paymentexpiredСессия истекла без оплаты
payment.manual_reviewdata.paymentmanual_reviewОперация ушла на ручную проверку
payment.refundeddata.paymentrefundedВозврат — полный или частичный (см. status)

subscription · payload root: data.subscription

subscription.createddata.subscriptioncreatedПодписка создана
subscription.updateddata.subscriptionupdatedИзменены параметры подписки
subscription.payment_succeededdata.subscriptionsucceededРекуррентное списание прошло
subscription.payment_faileddata.subscriptionfailedРекуррентное списание не прошло
subscription.past_duedata.subscriptionpast_dueПросрочка после неудачных попыток

События payment.partially_refunded не существует: частичный и полный возврат оба отправляют payment.refunded, различие — в поле status тела.

Проверка подписи

X-Webhook-Signature =
  v1=hmac_sha256(webhook_secret, X-Webhook-Timestamp + "." + raw_body)
# обработчик вебхука
raw = request.body                       # именно сырые bytes, не re-serialized JSON
expected = hmac_sha256(secret, ts + "." + raw)
if not constant_time_equals(expected, signature):
    return 401
if seen(request.headers["X-Webhook-Id"]):
    return 200                            # уже обработано, дубль игнорируем
process(json.loads(raw))
return 200

Порядок проверки, который обязан выполнять обработчик:

  1. прочитать сырое тело без разбора и повторной сериализации;
  2. убедиться, что X-Webhook-Timestamp — целое число секунд и его возраст не превышает 300 секунд;
  3. вычислить HMAC-SHA256 от строки «timestamp + точка + сырое тело»;
  4. сравнить с hex после v1= constant-time;
  5. разобрать JSON и проверить, что X-Webhook-Id совпадает с полем id конверта;
  6. атомарно отметить событие обработанным — и только затем выполнять бизнес-эффект.

Дедупликация

Доставка имеет семантику at-least-once: одно событие может прийти несколько раз. Дедупликация должна переживать перезапуск и деплой, поэтому in-memory кеша недостаточно.

BEGIN
  INSERT event_id ... ON CONFLICT DO NOTHING
  если вставлено 0 строк -> дубликат, вернуть 200
  обновить заказ только допустимым переходом статуса
  зафиксировать outbox или внутреннюю задачу
COMMIT
return 200

Вебхук не единственный источник для необратимой выдачи

Перед выдачей дорогого товара найдите заказ по сохранённому payment_id, при необходимости выполните подписанный GET /payments/{payment_id} и считайте оплату успешной только при status=succeeded и final=true. Возврат браузера и текст на странице подтверждением не являются.

Расписание повторов

Доставку подтверждает любой 2xx. Таймаут, сетевая ошибка, 3xx, 4xx и 5xx приводят к повтору по расписанию:

сразу
+1 минута
+5 минут
+30 минут
+2 часа
+6 часов
+24 часа
+48 часов

После исчерпания попыток доставка получает статус exhausted; из кабинета можно выполнить ручной replay. Replay создаёт новую доставку того же события: id и X-Webhook-Id остаются прежними, timestamp и подпись создаются заново. Корректно построенная дедупликация распознает дубликат и вернёт 2xx.

Ротация секрета

Ротация выдаёт новый секрет; старый и новый действуют одновременно до 24 часов. Порядок, при котором ни одно событие не теряется:

  1. научите проверяющий код принимать два секрета;
  2. запустите ротацию в кабинете и сохраните новый секрет;
  3. проверяйте сначала новым, затем старым;
  4. убедитесь, что новые доставки проходят проверку;
  5. после истечения перекрытия удалите старый секрет.

Не логируйте, какой именно секрет подошёл, если лог способен раскрыть его значение.

Была ли страница полезной?