PUBLIC API V1 · ДЛЯ РАЗРАБОТЧИКОВ
Визит гостя: предзаказ и открытый счёт
Предзаказ к брони, дозаказы в зале, свои цены и скидки, фиксация суммы и оплата в приложении.
Содержание
Сценарий
Гость бронирует стол в вашем приложении, заранее заказывает блюда, приходит в ресторан, официант добавляет чай или убирает блюдо, а гость в конце визита оплачивает окончательную сумму в приложении или на кассе. Всё это — один счёт визита в MenuLab.
Кто считает цены, решаете вы в каждом заказе, выбирать режим подключения не нужно:
- без цен в заказе цены, акции и скидки считает MenuLab по меню ресторана;
- с
pricing_source: "external"и ценами по строкам заказ считаете вы, MenuLab принимает ваш расчёт и не применяет свои акции. Бонусы MenuLab доступны в обоих случаях.
1. Предзаказ к брони
Создайте бронь (POST /api/v1/reservations), затем заказ с её номером:
curl -X POST "https://menulab.ru/api/v1/orders" \
-H "X-API-Key: $MENULAB_API_KEY" -H "X-API-Secret: $MENULAB_API_SECRET" \
-H "X-Idempotency-Key: app-visit-449" -H "Content-Type: application/json" \
-d '{
"type": "dine_in",
"source": "mobile_app",
"reservation_id": 568,
"external_id": "449",
"pricing_source": "external",
"pricing_contract_version": "2026-06-11.v1",
"schema_version": "2026-06-11.v1",
"items": [
{"external_id": "pizza-pepperoni", "quantity": 3, "guest_number": 1,
"unit_price": 700, "line_discount_amount": 210, "line_total": 1890}
],
"discount_amount": 210,
"discounts": [{"type": "promotion", "name": "3 пиццы −10%", "amount": 210}],
"external_total_before_bonus": 1890,
"allocation": {"discount_allocation_method": "external_line", "allocation_version": "2026-06-11.v1", "allocated_discount_total": 210}
}'
Заказ становится вкладкой предзаказа брони: стол, время визита и число гостей MenuLab берёт из брони. items[].guest_number — гость за столом, как на кассе. Без pricing_source и цен тот же запрос создаёт заказ по ценам MenuLab.
| Код | Когда |
|---|---|
RESERVATION_NOT_FOUND (404) |
Брони нет в этом ресторане |
RESERVATION_NOT_OPEN (409/422) |
Бронь отменена или завершена; тип заказа не dine_in |
RESERVATION_ALREADY_SEATED (409) |
Гости уже за столом. В meta.details.order_ids — заказы стола, дозаказ делайте в них |
2. Гости пришли: заказ стола
При посадке ресторан переносит предзаказ в заказ стола. Ваши цены и скидки переносятся вместе с позициями, заказ стола остаётся вашим: вы получаете его события.
- у предзаказа появляется
transferred_to_order_id— дальше работайте с этим заказом; GET /api/v1/reservations/{id}отдаётorder_id(основной заказ визита) иorders[]. Ресторан может открыть за столом несколько вкладок — они тоже вorders[].
3. Состав меняется: события и версия
У заказа есть revision — версия состава и денег. Она растёт при любой правке позиций или сумм и не меняется от статусов кухни. GET /api/v1/orders/{id} отдаёт её в поле revision и в заголовке ETag.
Когда официант добавляет, убирает позицию, меняет количество, модификаторы или гостя, приходит order.items_changed (подпишитесь на событие через /api/v1/webhooks):
{
"event_id": "…",
"event_type": "order.items_changed",
"data": {
"order_id": 3812,
"revision": 7,
"item_changes": [
{"item_id": 9921, "action": "added", "dish_id": 41, "quantity": 1, "guest_number": 2, "revision": 6}
],
"order": { "...": "полный заказ, как в GET /orders/{id}" }
}
}
action: added, removed (удалена или отменена), quantity_changed (с previous_quantity), modifiers_changed, guest_changed, price_changed. Серия нажатий официанта приходит одним событием. Событие с revision не новее уже обработанного — пропускайте. Полный актуальный состав всегда в data.order; отменённые позиции в нём есть со статусом cancelled или voided, удалённые до кухни — только в item_changes.
Если заказ считаете вы, после правки официанта у него pricing_status: "pending_reprice": ваши строки держат ваши цены и скидки, новые позиции стоят по меню MenuLab, итог — сумма строк. Заказ можно оплатить и в этом состоянии.
4. Новый расчёт: PUT /orders/{id}/pricing
Пересчитайте скидку по своим условиям и пришлите цены всех живых позиций заказа:
curl -X PUT "https://menulab.ru/api/v1/orders/3812/pricing" \
-H "X-API-Key: $MENULAB_API_KEY" -H "X-API-Secret: $MENULAB_API_SECRET" \
-H "If-Match: \"7\"" -H "Content-Type: application/json" \
-d '{
"pricing_contract_version": "2026-06-11.v1",
"schema_version": "2026-06-11.v1",
"items": [
{"order_item_id": 9917, "unit_price": 700, "line_discount_amount": 210, "line_total": 1890},
{"order_item_id": 9921, "unit_price": 150, "line_discount_amount": 150, "line_total": 0}
],
"discount_amount": 360,
"external_total_before_bonus": 1890,
"allocation": {"discount_allocation_method": "external_line", "allocation_version": "2026-06-11.v1", "allocated_discount_total": 360}
}'
Меняются только цены и скидки: позиции, их статусы на кухне и гости остаются как есть. Проверки сумм те же, что при создании заказа. Ответ — заказ с pricing_status: "accepted" и новой revision.
| Код | Когда |
|---|---|
REVISION_CONFLICT (409) |
Заказ изменился после вашей версии. Актуальная — в meta.details.revision: перечитайте заказ и пересчитайте |
REVISION_CONFLICT (428) |
Не передана версия (revision в теле или If-Match) |
PRICING_ITEMS_MISMATCH (422) |
Расчёт покрывает не все живые позиции. Нужные — в meta.details.order_item_ids |
VALIDATION_ERROR (422) |
Суммы не сходятся; подробности в error.errors |
ORDER_LOCKED (423) |
Счёт зафиксирован для оплаты |
ORDER_TRANSFERRED (409) |
Это перенесённый предзаказ, работайте с meta.details.order_id |
5. Дозаказ из приложения
POST /api/v1/orders/{id}/items — те же поля позиций, что при создании (dish_id/external_id, quantity, guest_number, modifiers, comment). Цены — по меню MenuLab; если заказ считаете вы, следом пришлите PUT /pricing.
POST /api/v1/orders/{id}/items/{itemId}/cancel убирает позицию, которая ещё не ушла на кухню. Отправленную на кухню отменяет персонал (ITEM_ALREADY_SENT, 409).
Обе операции принимают необязательную версию (revision или If-Match) и отвечают так же, как PUT /pricing.
6. Оплата в конце визита
Перед оплатой в приложении зафиксируйте состав и сумму:
curl -X POST "https://menulab.ru/api/v1/orders/3812/lock" \
-H "X-API-Key: $MENULAB_API_KEY" -H "X-API-Secret: $MENULAB_API_SECRET" \
-H "Content-Type: application/json" -d '{"revision": 8}'
После фиксации касса не меняет заказ, а ответ содержит итог к оплате и версию. Если заказ ждёт вашего расчёта — EXTERNAL_PRICING_REQUIRED (409): сначала PUT /pricing. Гость передумал — POST /orders/{id}/unlock.
Оплату ЮKassa подтвердите прежним POST /orders/{id}/mark-paid с суммой и версией:
{"payment_method": "online", "payment_provider": "yookassa", "payment_id": "2fbb8f0e-…", "amount": 1890, "revision": 8}
Если состав успел измениться, придёт REVISION_CONFLICT (409) с актуальными revision и total — оплату не отмечайте, покажите гостю новую сумму. Если гость платит на кассе, ничего делать не нужно: придёт order.paid.
Нужны ключи или права для интеграции? Их выдаёт администратор ресторана в бэк-офисе: Настройки → Интеграции → API-клиенты.