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

Публичный API

Отмена и онлайн-возвраты

Единый сценарий для отмены из интеграции и из POS.

Обновлено 20 августа 2026

Единый сценарий отмены и возврата

Для любого оплаченного заказа, созданного API-клиентом, MenuLab формирует refund_request_id и направляет события именно этому клиенту (order.api_client_id). Контракт работает для онлайн-оплаты, наличных, карты курьеру, терминала при получении и смешанной оплаты.

Кто фактически возвращает деньги, определяется полями payment_owner и action_required:

Оплата payment_owner action_required Кто возвращает деньги
Внешняя online-оплата, например YooKassa Giras external true API-интеграция
Наличные, касса, POS или банковский терминал MenuLab menulab_pos false Кассир/MenuLab/POS

Для обоих вариантов интеграция получает payment.refund.requested → payment.refund.succeeded либо payment.refund.requested → payment.refund.failed. Это разные бизнес-события с разными event_id, но с одним refund_request_id. Повторные попытки доставки одного события сохраняют его прежний event_id.

Для отмены нужен scope orders:write, для чтения запроса возврата — refunds:read, для подтверждения результата — refunds:write.

Отмена из интеграции

curl -X POST "https://restaurant.example/api/v1/orders/3812/cancel" \
  -H "X-API-Key: $MENULAB_API_KEY" \
  -H "X-API-Secret: $MENULAB_API_SECRET" \
  -H "X-Idempotency-Key: site-order-100-cancel" \
  -H "Content-Type: application/json" \
  -d '{"reason":"Клиент отказался от заказа"}'

Для оплаченного внешнего online-заказа ответ содержит объект data.refund:

{
  "success": true,
  "data": {
    "id": 3812,
    "status": "cancelled",
    "payment_status": "paid",
    "refund": {
      "refund_request_id": "dbd54426-eaa1-4d4c-995c-8e7ea9db0512",
      "status": "awaiting_external",
      "payment_owner": "external",
      "payment_method": "online",
      "action_required": true,
      "provider": "yookassa",
      "external_payment_id": "2fbb8f0e-000f-5000-8000-18db351245c7",
      "amount": "45.00",
      "amount_cents": 4500,
      "currency": "RUB"
    }
  }
}

До подтверждения движения денег payment_status остаётся paid, хотя заказ уже cancelled.

Возврат из POS MenuLab

Оператор оформляет полный или частичный возврат в CRM/POS. Для наличных, карты курьеру и терминала MenuLab сама фиксирует фактическую операцию и отправляет API-клиенту информационные события. Интеграции не нужно вызывать YooKassa или POST /refunds/{id}/confirm.

Пример данных события для наличного возврата:

{
  "refund_request_id": "3c69a750-d3a7-48e0-890f-e0651abefdb7",
  "order_id": 16150,
  "status": "succeeded",
  "payment_owner": "menulab_pos",
  "payment_method": "cash",
  "action_required": false,
  "provider": "menulab_pos",
  "external_payment_id": null,
  "amount": "45.00",
  "amount_cents": 4500,
  "currency": "RUB",
  "refunded_at": "2026-08-23T12:30:00+05:00"
}

external_payment_id обязателен только для внешней online-оплаты. Его отсутствие не блокирует события для наличных и терминальных способов оплаты.

Если возвращается уже выданный заказ, его производственный status может остаться completed, а payment_status станет refunded. Изменение также приходит в order.updated, поэтому состояние оплаты не следует выводить только из статуса заказа.

Выполнение внешнего online-возврата

Следующие действия нужны только при action_required: true. Из события используйте:

  • external_payment_id — ID исходного платежа;
  • amount_cents и currency — ожидаемую сумму (amount оставлено для отображения);
  • refund_request_id — ключ дедупликации и идемпотентности.

Сначала надёжно сохраните событие и ответьте MenuLab HTTP 2xx. Возврат у провайдера выполняйте из фоновой очереди.

Подтверждение внешнего результата

После успешного ответа провайдера:

curl -X POST "https://restaurant.example/api/v1/refunds/dbd54426-eaa1-4d4c-995c-8e7ea9db0512/confirm" \
  -H "X-API-Key: $MENULAB_API_KEY" \
  -H "X-API-Secret: $MENULAB_API_SECRET" \
  -H "X-Idempotency-Key: dbd54426-succeeded" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "succeeded",
    "amount": 45.00,
    "external_refund_id": "2fbb8f0e-0015-5000-9000-1c7ea95d6d98",
    "refunded_at": "2026-08-20T15:07:08+05:00"
  }'

После этого MenuLab устанавливает payment_status: "refunded", фиксирует одну операцию возврата, сверяет бонусы и отправляет payment.refund.succeeded.

Не вызывайте POST /api/v1/payments/refund, если деньги возвращает внешняя интеграция. Не вызывайте endpoint подтверждения для payment_owner: menulab_pos: его завершает MenuLab.

Статусы возврата

Статус Что означает Действие интеграции
awaiting_external MenuLab ждёт владельца платежа Поставить запрос в очередь
processing Запрос принят интеграцией Проверять провайдера, не создавать второй возврат
succeeded Результат подтверждён Терминальный успех
failed Провайдер однозначно отказал Исправить причину и продолжить тот же запрос
manual_review Нужна ручная сверка Не создавать возврат без проверки

При таймауте провайдера не отправляйте failed и не создавайте новый возврат. Сначала получите состояние ранее созданной операции по ключу идемпотентности.

После однозначного failed тот же внешний refund_request_id можно снова перевести в processing, а затем подтвердить как succeeded. Событие payment.refund.failed создаётся MenuLab после подтверждения failed интеграцией; неопределённый ответ YooKassa сам по себе не является окончательной ошибкой.