M MenuLabБаза знаний Поддержка

Публичный 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.

Правильная обработка

  1. Проверьте подпись и допустимый возраст события.
  2. Сохраните raw body и event_id в БД или очереди.
  3. Если event_id уже сохранён, верните 2xx без повторного бизнес-действия.
  4. Верните 2xx сразу после надёжного сохранения.
  5. Обрабатывайте событие асинхронно.

Доставка выполняется как минимум один раз. Строгий порядок разных событий не гарантируется, поэтому обработчик обязан быть идемпотентным и сверять актуальное состояние.

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 отдельно запрашивать бонусный баланс не нужно.