Документация/Гайды/Вебхуки
Вебхуки
Каталог событий генерируется из кода доставки — таблица не может разойтись с тем, что реально отправляет backend.
Требования к endpoint
- только публичный HTTPS URL, без учётных данных в самом URL;
- DNS не должен указывать на private-, loopback- или link-local-адрес;
- endpoint не должен отвечать редиректом;
2xxвозвращается быстро и только после надёжной записи события в очередь или БД; тяжёлая обработка — асинхронно.
Источник доверия — подпись, а не адрес отправителя. Не ограничивайте свой endpoint по IP платформы: адреса доставки не входят в контракт и меняются.
События
payment · payload root: data.payment
| payment.created | data.payment | created | Сессия создана |
| payment.processing | data.payment | processing | Провайдер обрабатывает операцию |
| payment.requires_action | data.payment | requires_action | Нужен 3-DS или OTP плательщика |
| payment.authorized | data.payment | authorized | Средства захолдированы, не списаны |
| payment.succeeded | data.payment | succeeded | Оплата завершена, деньги зафиксированы |
| payment.declined | data.payment | declined | Отклонено эмитентом или антифродом |
| payment.failed | data.payment | failed | Техническая ошибка проведения |
| payment.canceled | data.payment | canceled | Отменено до списания |
| payment.expired | data.payment | expired | Сессия истекла без оплаты |
| payment.manual_review | data.payment | manual_review | Операция ушла на ручную проверку |
| payment.refunded | data.payment | refunded | Возврат — полный или частичный (см. status) |
subscription · payload root: data.subscription
| subscription.created | data.subscription | created | Подписка создана |
| subscription.updated | data.subscription | updated | Изменены параметры подписки |
| subscription.payment_succeeded | data.subscription | succeeded | Рекуррентное списание прошло |
| subscription.payment_failed | data.subscription | failed | Рекуррентное списание не прошло |
| subscription.past_due | data.subscription | past_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Порядок проверки, который обязан выполнять обработчик:
- прочитать сырое тело без разбора и повторной сериализации;
- убедиться, что
X-Webhook-Timestamp— целое число секунд и его возраст не превышает 300 секунд; - вычислить HMAC-SHA256 от строки «timestamp + точка + сырое тело»;
- сравнить с hex после
v1=constant-time; - разобрать JSON и проверить, что
X-Webhook-Idсовпадает с полемidконверта; - атомарно отметить событие обработанным — и только затем выполнять бизнес-эффект.
Дедупликация
Доставка имеет семантику 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 часов. Порядок, при котором ни одно событие не теряется:
- научите проверяющий код принимать два секрета;
- запустите ротацию в кабинете и сохраните новый секрет;
- проверяйте сначала новым, затем старым;
- убедитесь, что новые доставки проходят проверку;
- после истечения перекрытия удалите старый секрет.
Не логируйте, какой именно секрет подошёл, если лог способен раскрыть его значение.