Приём вебхуков
Вебхук - это наш POST-запрос на ваш адрес в момент, когда что-то произошло: заказ создан, оплачен, собран, отменён. Опрашивать /v1/orders по таймеру при этом не нужно.
Как включить
- В кабинете укажите адрес приёмника. Только
https, только порт 443, самоподписанный сертификат мы не примем. - Отметьте события, которые вам нужны. Подписка на всё подряд превращает журнал в шум.
- Скопируйте секрет подписи. Он показывается один раз и меняется отдельной кнопкой.
- Отправьте тестовое событие из кабинета и убедитесь, что приёмник ответил 2xx.
Что приходит
POST /hooks/nimbus HTTP/1.1 Content-Type: application/json X-Event-Id: 0f1c8ad4-5b21-4f77-9a30-2c6e1b884d10 X-Event-Type: order.paid X-Event-Time: 2026-08-16T11:20:41Z X-Signature: sha256=9c1f0b4e... { "id": "0f1c8ad4-5b21-4f77-9a30-2c6e1b884d10", "type": "order.paid", "order": { "id": "ORD-25161", "total": 387000, "items": [ { "sku": "TRM-1579", "qty": 3 } ] } }
Тело всегда JSON в UTF-8, время всегда UTC. Полей в объекте order со временем становится больше, поэтому разбирайте его по именам и не падайте на незнакомом ключе.
Проверка подписи
Подпись считается как HMAC SHA-256 от сырого тела запроса. Считать её по разобранному и заново собранному JSON нельзя: порядок ключей изменится, и подпись не сойдётся.
import crypto from 'node:crypto'; export function valid(rawBody, header, secret) { const mine = crypto.createHmac('sha256', secret).update(rawBody).digest('hex'); const sent = String(header || '').replace('sha256=', ''); const a = Buffer.from(mine, 'utf8'); const b = Buffer.from(sent, 'utf8'); return a.length === b.length && crypto.timingSafeEqual(a, b); }
import hashlib, hmac def valid(raw_body: bytes, header: str, secret: str) -> bool: mine = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() sent = (header or "").replace("sha256=", "") return hmac.compare_digest(mine, sent)
function valid(string $raw, string $header, string $secret): bool { $mine = hash_hmac('sha256', $raw, $secret); $sent = str_replace('sha256=', '', $header); return hash_equals($mine, $sent); }
timingSafeEqual и его аналоги существуют ровно для этого.Повторы и идемпотентность
Мы ждём ответ 5 секунд. Любой ответ 2xx считаем принятым, всё остальное - неудачей и повторяем доставку шесть раз: через минуту, 5 минут, 15 минут, час, 6 часов и сутки. После последней попытки подписка ставится на паузу, а в кабинет приходит уведомление.
X-Event-Id тот же самый. Храните принятые идентификаторы 72 часа и отвечайте на повтор 2xx, ничего не делая: сеть рвётся чаще, чем кажется, и заказ, собранный дважды, стоит дороже лишней таблицы.События
| Событие | Когда приходит |
|---|---|
order.created | Заказ создан на витрине, оплата ещё не подтверждена |
order.paid | Оплата подтверждена, заказ можно собирать |
order.assembled | Сборка завершена, ждём передачу в доставку |
order.shipped | Заказ передан в доставку, есть трек-номер |
order.canceled | Отмена целиком или последней позиции заказа |
stock.low | Остаток позиции опустился ниже порога, заданного в кабинете |
price.rejected | Цена не принята витриной: чаще всего ниже минимальной |
Полный список с телом каждого события - в справочнике, раздел события.
Отладка
Журнал доставок в кабинете хранит 30 дней: по каждому событию видно тело, заголовки, код ответа и время. Повторить доставку можно кнопкой - это нормальный способ починить приёмник после релиза, не дожидаясь следующего заказа.
Если приёмник отвечает 2xx, но событие потерялось у вас, смотрите X-Event-Id в своих журналах: он же лежит в журнале доставок и в ответе метода /v1/hooks/deliveries.