Skip to Content
WebhooksОбзор

Webhooks — Обзор

Webhook’и — авторитетный сигнал. Браузерные callback’и (onSuccess) и представления панели — удобство; webhook’и — истина.

Гарантии доставки

  • Не менее одного раза. Одно событие может быть доставлено до 6 раз, если ваш сервер не возвращает 2xx в течение таймаута. Делайте обработчик идемпотентным — дедуплицируйте по X-Delivery (payload не имеет поля event_id; стабильный delivery UUID — это ключ идемпотентности).
  • Одно событие на HTTP-запрос. Без батчинга.
  • Изоляция per-endpoint. Если у вас несколько зарегистрированных endpoint’ов, у каждого свой трек доставки + повторов. Один медленный URL мерчанта не может голодать другие — у каждого хоста собственный circuit breaker.
  • Подписано. Каждый payload несёт заголовок X-Signature (а в течение 24-часового окна после ротации — также X-Signature-Prev). Проверяйте до того, как что-либо делать с телом. См. Проверка подписи.

Подписываемые типы событий

СобытиеСрабатывает когда…
payment.settledOn-chain перевод набрал число подтверждений сети. Используйте это, чтобы отметить заказы оплаченными.
payment.failedФиатный платёж был явно отклонён провайдером (сейчас: Stripe webhook, сигнализирующий о неудаче). Не срабатывает для крипто-таймаутов — те всплывают как checkout.expired, а короткие крипто-платежи как payment.underpaid.
payment.underpaidСредства пришли, но меньше общей суммы заказа (типично: комиссия за перевод стейблкоина вычтена из суммы).
payment.overpaidСредства пришли в избытке от общей суммы заказа. Излишек записан, но не возвращается автоматически.
order.createdОткрыт новый заказ — либо через ваш B2B API-вызов, либо через конверсию checkout-сессии.
order.canceledЗаказ перешёл в отменён. data.reason в payload’е отличает ручную отмену от payment_timeout (устаревший неоплаченный заказ, выметенный worker’ом).
order.resolvedЗаказ PARTIAL_PAID был разрешён в PAID — мерчант принял недоплату.
order.reopenedРанее авто-отменённый заказ (canceled_reason=payment_timeout) был переоткрыт мерчантом.
checkout.createdПокупатель открыл checkout для заказа.
checkout.completedПоток на стороне покупателя завершён (не подразумевает on-chain расчёта — используйте payment.settled для этого).
checkout.expiredПокупатель забросил, и TTL сессии истёк.
payment.refund.requestedСоздана запись возврата — либо из инициированного мерчантом API-вызова, либо из отправленной клиентом формы refund-request.
payment.refund.approvedОжидающий возврат прошёл ваш workflow одобрения.
payment.refund.rejectedОжидающий возврат отклонён.
payment.refund.executedOn-chain перевод возврата прошёл, и запись перешла в терминальное executed.
refund_request.createdRefund-request токен был выпущен. data.source равно b2b / dashboard / renewal. Подписка опциональна — полезно для аудиторских pipeline’ов, отслеживающих, какой токен сейчас активен per заказ.
refund_request.renewal_requestedПокупатель нажал «Запросить новую ссылку» после истечения токена. Настоятельно рекомендуется подписаться — это сигнал мерчанту, что в виджете обновлений появился новый элемент для действия.
refund_request.renewedОбновление было одобрено, и новый токен заменил старый. data.old_token / data.new_token формируют аудиторскую цепочку.
refund_request.canceledМерчант перевёл токен в CANCELED из панели (например, отклонил запрос на обновление, убил живую ссылку). Идемпотентно — отправляется только при первом переходе. data.reason — опциональная заметка мерчанта.

Панель получает этот список из GET /v1/webhooks/event-types, чтобы форма создания / редактирования endpoint’а всегда соответствовала тому, что платформа реально отправляет. Подписка на событие, которое мы не отправляем, будет отклонена на этапе создания с чёткой ошибкой.

Тестовые события не подписываемы. Кнопка Send Test per-endpoint в панели делает POST конверта webhook.test.ping синхронно на этот один endpoint (минуя pipeline повторов), а legacy-путь на уровне мерчанта «Send test event» делает fan-out конверта webhook.test на каждый активный endpoint независимо от его фильтра. Ни одно из них не появляется в каталоге выше — вы получаете их по факту наличия зарегистрированного endpoint’а, а не подписавшись.

Подписывайтесь только на события, которые вы обрабатываете. У каждого endpoint’а свой фильтр событий; wildcard "*" означает «каждое событие, включая добавленные в будущем». Подписка на меньшее число событий держит ваш обработчик чище и уменьшает поверхность, которую нам нужно повторять при ошибках.

Payload + заголовки

HTTP-тело — это напрямую объект данных конкретного события. Нет внешнего Stripe-стиля конверта — поля вроде типа события, delivery ID и timestamp эмиссии живут в заголовках. Для payment.settled тело выглядит так:

{ "receipt_id": "rcp_…", "order_id": "ord_…", "payment_intent_id": "pin_…", "checkout_session_id": "cst_…", "merchant_id": "mer_…", "customer_id": "cus_…", "total": "49.00", "currency": "USD", "payment_method": "crypto", "token": "USDC", "network": "polygon", "tx_hash": "0x…", "deposit_address": "0x…", "treasury_address": "0x…", "amount_received": "49.00", "confirmations": 5, "metadata": { /* per-event */ } }

Другие события несут свой набор полей — см. publisher-структуры в payment-service/internal/domain/events.go для канонической формы, пока per-event документация не появится. Имена полей стабильны (нижний snake_case); on-chain tx hash всегда tx_hash (не transaction_hash).

Заголовки входящего запроса

Content-Type: application/json X-Event: payment.settled X-Delivery: 7c9e6679-7425-40de-944b-e07fc1f90ae7 Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7 X-Timestamp: 1729536000 X-Signature: sha256=9a8b7c… X-Signature-Prev: sha256=fa31b2… (только в течение grace-окна ротации)
ЗаголовокЧто это
X-EventТип события (например, payment.settled). Маршрутизируйте по нему на уровне прокси, если хотите пропустить парсинг JSON.
X-DeliveryUUID, идентифицирующий строку доставки. Стабилен между всеми повторами одной пары (событие, endpoint) — используйте его как ключ идемпотентности.
Idempotency-KeyЗеркалит X-Delivery (то же значение). Установлен на каждой доставке — перенимает конвенцию из Stripe / GitHub.
X-TimestampUnix-секунды отправки попытки. Подписан в payload, чтобы захваченную пару (тело, X-Signature) нельзя было replay’ить бесконечно — отклоняйте доставки, у которых timestamp вне вашего окна допуска.
X-Signaturesha256=<hex> от HMAC-SHA256(secret, X-Timestamp + "." + raw_body). См. Проверка подписи.
X-Signature-PrevТот же алгоритм с предыдущим секретом. Присутствует только в 24-часовом окне после ротации — позволяет verifier’ам, запущенным с любым из ключей, продолжать принимать доставки во время cutover. После закрытия окна заголовок перестаёт отправляться.

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

Если ваш endpoint не возвращает 2xx в течение таймаута, мы повторяем по этому расписанию (timestamp’ы относительно первой попытки):

ПопыткаЗадержкаНакопительно
1
2+1 мин
3+5 мин
4+15 мин21м
5+1 час1ч 21м
6+6 часов7ч 21м

После того как попытка 6 не удалась, доставка переводится в dead letter, и email мерчантского аккаунта уведомляется. Dead-lettered события можно проиграть заново из панели Developers → Webhooks → Delivery history или напрямую через POST /v1/webhooks/deliveries/:id/replay. Каждый replay создаёт новую строку доставки со своим X-Delivery — аудиторская цепочка возвращается к оригиналу через parent_delivery_id, чтобы повторы replay’ев не затеняли исходное событие.

Регистрация endpoint’а

Из панели мерчанта :

  1. Developers → Webhooks+ Add endpoint
  2. Вставьте ваш URL — только https://… (обычный HTTP отклоняется; форма создания также блокирует localhost, частные IP-диапазоны и URL с userinfo)
  3. Выберите события для подписки (или * для всех)
  4. Выберите окружение — test или live (у каждого свой секрет; они никогда не пересекаются)
  5. Save → панель отображает секрет подписи (whsec_…) один раз. Сохраните его на сервере; он понадобится для следующих двух фич.

Вы можете зарегистрировать до 10 endpoint’ов на окружение на мерчанта (например, один для обработки в продакшене, один для зеркалирования в staging, один для уведомлений в Slack). У каждого своё состояние повторов, секрет и per-host circuit breaker.

Действия жизненного цикла на каждом endpoint’е

Меню ⋮ на карточке каждого endpoint’а показывает:

  • Edit — изменить URL, описание или список подписок. Новый URL пере-валидируется теми же правилами https:///SSRF, что и при создании.
  • Send Test — синхронный POST конверта webhook.test.ping, подписанный вашим текущим секретом. Панель показывает HTTP-статус, latency и 512-байтный сниппет вашего ответа. Минует RMQ-pipeline, чтобы ответ был мгновенным.
  • Rotate Secret — генерирует новый секрет. Предыдущий остаётся валидным 24 часа (доставки несут и X-Signature, и X-Signature-Prev в окне, чтобы verifier’ы, работающие с любым из ключей, продолжали принимать события, пока вы переразворачиваете).
  • Reveal Secret — повторно отображает существующий секрет. Защищён свежей 2FA-проверкой и записывается в журнал аудита; используйте, только когда вы потеряли копию и Rotate неприемлем.
  • Enable / Disable — переключает is_active без потери истории доставки. Отключённые endpoint’ы остаются в панели, но не получают новых доставок.
  • Delete — необратимо. Используйте Disable, если можете переключиться обратно позже.

Советы для обработчиков

  1. Возвращайте 2xx быстро. Подтверждайте 200 OK до тяжёлой работы — кидайте обработку в фоновую очередь. Таймаут per-попытка — 10 секунд; удержание ответа дольше этого триггерит повтор. Таймаут на стороне платформы и не настраивается мерчантом — пишите в поддержку, если ваш обработчик действительно нуждается в большем времени.
  2. Дедуплицируйте по X-Delivery (или Idempotency-Key — то же значение). Даже если вы возвращаете 2xx, upstream-прокси может уронить соединение и спровоцировать повтор; delivery ID стабилен между каждым повтором одной строки доставки, поэтому это правильный ключ.
  3. Толерируйте неизвестные типы событий. Новые события могут появиться; возвращайте 200 и no-op, а не 4xx, иначе заполните очередь повторов.
  4. Логируйте X-Delivery рядом с бизнес-логикой. Когда что-то пошло не так, это ключ join между нашей стороной и вашей.

Что дальше