Webhooks — Обзор
Webhook’и — авторитетный сигнал. Браузерные callback’и
(onSuccess) и представления панели — удобство; webhook’и — истина.
Гарантии доставки
- Не менее одного раза. Одно событие может быть доставлено до
6 раз, если ваш сервер не возвращает 2xx в течение таймаута.
Делайте обработчик идемпотентным — дедуплицируйте по
X-Delivery(payload не имеет поляevent_id; стабильный delivery UUID — это ключ идемпотентности). - Одно событие на HTTP-запрос. Без батчинга.
- Изоляция per-endpoint. Если у вас несколько зарегистрированных endpoint’ов, у каждого свой трек доставки и повторов. Один медленный endpoint не задерживает остальные.
- Подписано. Каждый payload несёт заголовок
X-Signature(а в течение 24-часового окна после ротации — такжеX-Signature-Prev). Проверяйте до того, как что-либо делать с телом. См. Проверка подписи.
Подписываемые типы событий
| Событие | Срабатывает когда… |
|---|---|
payment.settled | On-chain перевод набрал число подтверждений сети. Используйте это, чтобы отметить заказы оплаченными. |
payment.failed | Фиатный платёж был отклонён платёжным провайдером. Не срабатывает для крипто-таймаутов — те всплывают как checkout.expired, а короткие крипто-платежи как payment.underpaid. |
payment.underpaid | Средства пришли, но меньше общей суммы заказа (типично: комиссия за перевод стейблкоина вычтена из суммы). |
payment.overpaid | Средства пришли в избытке от общей суммы заказа. Излишек записан, но не возвращается автоматически. |
order.created | Открыт новый заказ — либо через ваш B2B API-вызов, либо через конверсию checkout-сессии. |
order.canceled | Заказ перешёл в отменён. data.reason в payload’е отличает ручную отмену от payment_timeout (неоплаченный заказ истёк по времени). |
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.executed | On-chain перевод возврата прошёл, и запись перешла в терминальное executed. |
refund_request.created | Refund-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 — опциональная заметка мерчанта. |
Запланировано (скоро)
Скоро. Эти события относятся к регулярным счетам и подпискам, которые пока недоступны. Их нет в таблице подписываемых событий выше, и подписаться на них сегодня нельзя. См. Регулярные счета.
| Планируемое событие | Срабатывает, когда… |
|---|---|
subscription.created | Создана подписка. |
invoice.created | Создан счёт за расчётный период. |
invoice.paid | Счёт оплачен. |
subscription.past_due | Счёт не оплачен после срока оплаты. |
subscription.canceled | Подписка отменена. |
Форма endpoint’а в панели перечисляет те же события. Подписка на несуществующее событие отклоняется при сохранении endpoint’а.
Тестовые события не подписываемы. Кнопка Send Test per-endpoint
в панели сразу, без повторов, отправляет событие webhook.test.ping на
этот один endpoint. Его нет в каталоге выше: вы получаете его потому, что
у вас есть зарегистрированный endpoint, а не потому, что подписались.
Подписывайтесь только на события, которые вы обрабатываете. У
каждого endpoint’а свой фильтр событий; wildcard "*" означает
«каждое событие, включая добавленные в будущем». Подписка на
меньшее число событий упрощает ваш обработчик и означает меньше
повторов, когда на вашем endpoint’е возникают ошибки.
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 */ }
}Другие события несут свои поля. Имена полей стабильны (нижний
snake_case); on-chain хеш транзакции всегда tx_hash.
tx_hash — идентификатор транзакции в собственном формате сети (0x… в EVM-сетях; нативный хеш или подпись в TRON, Solana и TON). deposit_address может отсутствовать для TRON, Solana и TON, поскольку там покупатели платят напрямую на ваш кошелёк Казначейства, а confirmations соответствует Сети и активы.
Заголовки входящего запроса
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-Delivery | UUID, идентифицирующий строку доставки. Стабилен между всеми повторами одной пары (событие, endpoint) — используйте его как ключ идемпотентности. |
Idempotency-Key | Зеркалит X-Delivery (то же значение). Установлен на каждой доставке. |
X-Timestamp | Unix-секунды отправки попытки. Подписан в payload, чтобы захваченную пару (тело, X-Signature) нельзя было replay’ить бесконечно — отклоняйте доставки, у которых timestamp вне вашего окна допуска. |
X-Signature | sha256=<hex> от HMAC-SHA256(secret, X-Timestamp + "." + raw_body). См. Проверка подписи. |
X-Signature-Prev | Тот же алгоритм с предыдущим секретом. Присутствует только в 24-часовом окне после ротации — позволяет verifier’ам, запущенным с любым из ключей, продолжать принимать доставки во время cutover. После закрытия окна заголовок перестаёт отправляться. |
Расписание повторов
Если ваш endpoint не возвращает 2xx в течение таймаута, мы повторяем
по этому расписанию (timestamp’ы относительно первой попытки):
| Попытка | Задержка | Накопительно |
|---|---|---|
| 1 | 0с | 0с |
| 2 | +1 мин | 1м |
| 3 | +5 мин | 6м |
| 4 | +15 мин | 21м |
| 5 | +1 час | 1ч 21м |
| 6 | +6 часов | 7ч 21м |
После того как попытка 6 не удалась, доставка переводится в
Failed, и email вашего аккаунта уведомляется.
Неудавшиеся события можно повторить из панели Developers →
Webhooks → Delivery history. Каждый replay — это новая доставка со
своим X-Delivery.
Регистрация endpoint’а
Из панели мерчанта :
- Developers → Webhooks → + Add endpoint
- Вставьте ваш URL — только
https://…(обычный HTTP отклоняется; форма создания также блокируетlocalhost, частные IP-диапазоны и URL с userinfo) - Выберите события для подписки (или
*для всех) - Выберите окружение — test или live (у каждого свой секрет; они никогда не пересекаются)
- Save → панель отображает секрет подписи (
whsec_…) один раз. Сохраните его на сервере; он понадобится для следующих двух фич.
Вы можете зарегистрировать до 10 endpoint’ов на окружение на мерчанта (например, один для обработки в продакшене, один для зеркалирования в staging, один для уведомлений в Slack). У каждого своё состояние повторов и секрет.
Действия жизненного цикла на каждом endpoint’е
Меню ⋮ на карточке каждого endpoint’а показывает:
- Edit — изменить URL, описание или список подписок. Новый URL
пере-валидируется теми же правилами
https:///SSRF, что и при создании. - Send Test — синхронный POST конверта
webhook.test.ping, подписанный вашим текущим секретом. Панель показывает HTTP-статус, latency и 512-байтный сниппет вашего ответа. Тестовые пинги не повторяются, поэтому ответ мгновенный. - Rotate Secret — генерирует новый секрет. Предыдущий остаётся
валидным 24 часа (доставки несут и
X-Signature, иX-Signature-Prevв окне, чтобы verifier’ы, работающие с любым из ключей, продолжали принимать события, пока вы переразворачиваете). - Reveal Secret — повторно отображает существующий секрет. Защищён свежей 2FA-проверкой и записывается в журнал аудита; используйте, только когда вы потеряли копию и Rotate неприемлем.
- Enable / Disable — включает или отключает endpoint без потери истории доставки. Отключённые endpoint’ы остаются в панели, но не получают новых доставок.
- Delete — необратимо. Используйте Disable, если можете переключиться обратно позже.
Советы для обработчиков
- Возвращайте 2xx быстро. Подтверждайте
200 OKдо тяжёлой работы — переносите обработку в фоновую задачу. Таймаут per-попытка — 10 секунд; удержание ответа дольше этого триггерит повтор. Таймаут на стороне платформы и не настраивается мерчантом — пишите в поддержку, если ваш обработчик действительно нуждается в большем времени. - Дедуплицируйте по
X-Delivery(илиIdempotency-Key— то же значение). Даже если вы возвращаете 2xx, upstream-прокси может уронить соединение и спровоцировать повтор; delivery ID стабилен между каждым повтором одной строки доставки, поэтому это правильный ключ. - Толерируйте неизвестные типы событий. Новые события могут появиться; возвращайте 200 и no-op, а не 4xx, иначе эти доставки будут повторяться снова и снова.
- Логируйте
X-Deliveryрядом с бизнес-логикой. Когда что-то пошло не так, это ключ join между нашей стороной и вашей.
Что дальше
- Проверка подписи — точный алгоритм + паттерны защиты от replay.
- Концепции → Сессии — в каком состоянии сессия, когда срабатывает каждое событие.