Публичный 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 сам по себе не является окончательной ошибкой.