MenuLabБаза знаний Быстрый старт API

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-клиенты.