Публичный API
Webhook-события
Подпись, доставка, повторы и дедупликация.
Обновлено 20 августа 2026События
Для заказов используйте:
order.created;order.updated;order.paid;order.completed;order.cancelled.
Для возвратов всех способов оплаты обязательны payment.refund.requested,
payment.refund.succeeded и payment.refund.failed. Проверяйте
data.action_required: только значение true требует вызова внешнего эквайера.
Завершение заказа и бонусы
Точное событие завершения — order.completed. Оно создаётся после фиксации бонусного журнала, поэтому дополнительный polling баланса не требуется.
{
"event_id": "488eb7bc-f8db-4376-a6ef-cafad8f9fe3d",
"event_type": "order.completed",
"timestamp": "2026-08-20T15:07:01+05:00",
"data": {
"order_id": 3812,
"external_id": "giras-order-742",
"status": "completed",
"changes": {
"status": {"from": "ready", "to": "completed"}
},
"loyalty": {
"customer_id": 42,
"bonus_earned": "50.00",
"bonus_earned_cents": 5000,
"bonus_spent": "100.00",
"bonus_spent_cents": 10000,
"bonus_balance": "250.00",
"bonus_balance_cents": 25000
}
}
}
bonus_earned включает фактическое начисление и бонусные промоакции заказа. bonus_spent — итоговое списание после корректировок. bonus_balance — новый баланс клиента. Для денежных вычислений используйте целочисленные поля *_cents.
Для заказа без клиента блок loyalty также присутствует: начисление и списание равны нулю, а customer_id и баланс — null.
Формат события
{
"event_id": "488eb7bc-f8db-4376-a6ef-cafad8f9fe3d",
"event_type": "payment.refund.requested",
"timestamp": "2026-08-20T15:07:01+05:00",
"data": {
"refund_request_id": "dbd54426-eaa1-4d4c-995c-8e7ea9db0512",
"order_id": 3812,
"payment_owner": "external",
"payment_method": "online",
"action_required": true,
"external_payment_id": "2fbb8f0e-000f-5000-8000-18db351245c7",
"amount": "45.00",
"amount_cents": 4500,
"currency": "RUB"
}
}
Проверка подписи
MenuLab подписывает точное исходное тело запроса HMAC SHA-256. Проверяйте X-MenuLab-Signature до обработки JSON.
import crypto from "node:crypto";
function verifyMenulabWebhook(rawBody, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(signature, "hex")
);
}
Не вычисляйте подпись от JSON.stringify(req.body): повторная сериализация может изменить байты.
Полезные заголовки: X-MenuLab-Event-ID, X-MenuLab-Event, X-MenuLab-Signature, X-MenuLab-Timestamp, X-MenuLab-Delivery-Attempt.
Правильная обработка
- Проверьте подпись и допустимый возраст события.
- Сохраните raw body и
event_idв БД или очереди. - Если
event_idуже сохранён, верните2xxбез повторного бизнес-действия. - Верните
2xxсразу после надёжного сохранения. - Обрабатывайте событие асинхронно.
Доставка выполняется как минимум один раз. Строгий порядок разных событий не гарантируется, поэтому обработчик обязан быть идемпотентным и сверять актуальное состояние.
requested, succeeded и failed — отдельные события, поэтому их event_id
различаются. Все они связаны одним refund_request_id. При повторной доставке
конкретного события MenuLab сохраняет его исходный event_id.
Если webhook временно недоступен, MenuLab делает первую попытку немедленно и до восьми повторов через 1, 5, 30, 60, 120, 240, 480 и 1440 минут. Всего — до девяти попыток; событие хранится до 7 дней. Все попытки имеют один event_id, а номер передаётся в X-MenuLab-Delivery-Attempt. После исправления endpoint доставку можно повторить вручную из CRM.
Polling используйте как канал восстановления, а не как основной источник событий. После order.completed с блоком loyalty отдельно запрашивать бонусный баланс не нужно.