RestoPOS · REST API v1

API RestoPOS для разработчиков

Справочник по интеграции с облачной POS-системой RestoPOS: приём заказов из внешних витрин и агрегаторов, чтение меню и остатков, работа с заказами, клиентами и доставкой.

База
https://pos.forris.uz/api/v1
Формат
JSON
Авторизация
Bearer (Laravel Sanctum)

Обзор

Все методы живут под /api/v1 и отвечают JSON. Организация определяется по токену — передавать её идентификатор не нужно и нельзя.

Данные разделены на двух уровнях. Организация берётся из токена: увидеть чужие данные нельзя даже при подстановке идентификаторов. Филиал для большинства методов задаётся заголовком X-Branch-Id — токен может иметь доступ к нескольким филиалам, и система должна понимать, о каком идёт речь.

Исключение — приём внешних заказов

Метод приёма заказов работает без X-Branch-Id: филиал приходит в теле запроса. Так витрина или агрегатор может слать заказы во все точки организации одним токеном.

Аутентификация

Bearer-токены Sanctum. Токен передаётся заголовком Authorization: Bearer <token> в каждом запросе.

POST/auth/login

Вход по логину и паролю. Без авторизации.

Запрос
{
  "login":       "manager@example.com",
  "password":    "secret",
  "device_name": "Integration server"
}
Ответ 200
{
  "message": "Успешная авторизация.",
  "data": {
    "user":       { "id": 12, "name": "Иван Петров", "…": "…" },
    "token":      "17|xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "expires_at": "2026-09-06T10:00:00+05:00"
  }
}
Ответ 422 — неверные данные
{ "message": "Ошибка авторизации.", "errors": { "login": ["…"] } }
POST/auth/pin-login

Вход по PIN-коду — для кассовых терминалов, где не набирают пароль.

GET/auth/me

Текущий пользователь, его организация, филиалы и права.

POST/auth/logout

Отзывает текущий токен. /auth/logout-all отзывает все токены пользователя.

Токены для интеграций

Для сервер-серверных интеграций логин с паролем не нужен и вреден: пароль сотрудника меняется, токен умирает. Вместо этого выпускается служебный токен с ограниченными правами — командой на стороне ресторана:

php artisan integration:create-token

Такой токен получает права integration:read и integration:write. Метод приёма заказов требует именно integration:write — обычный пользовательский токен его не откроет.

Заголовки

ЗаголовокКогда нужен
Authorization Bearer <token> — везде, кроме входа и регистрации.
Accept application/json — иначе на ошибках валидации вернётся HTML вместо JSON.
X-Branch-Id Филиал для методов меню, заказов, склада, кассы, доставки. Альтернатива — поле branch_id в теле запроса или параметре маршрута.
Idempotency-Key Обязателен на записи через /integrations/*. Формат — UUID. Тот же ключ с тем же телом вернёт исходный результат, с другим — конфликт.

Ответы и ошибки

Успешный ответ содержит data, ошибка — message и подробности. Списки приходят с пагинацией: параметры page и per_page, максимум 100 на страницу.

Успех
{ "success": true, "data": { "…": "…" }, "message": "Order accepted" }
Ошибка бизнес-логики
{
  "success": false,
  "error": {
    "code":    "UNKNOWN_PRODUCTS",
    "message": "Товары не найдены: 412, 980.",
    "details": { "product_ids": [412, 980] }
  }
}
Ошибка валидации — стандартный формат Laravel
{ "message": "…", "errors": { "items": ["Заказ не может быть пустым."] } }
КодЗначение
200успешно; для повторного заказа — уже принят ранее
201создано
401токен не передан, истёк или отозван
403у токена нет нужного права или доступа к филиалу
404объект не найден или принадлежит другой организации
409конфликт идемпотентности
422ошибка валидации или бизнес-правила
429превышен лимит частоты

Лимиты

ГруппаЛимитСчитается по
Вход и регистрация5 / минIP-адресу
Остальные методы60 / минпользователю, иначе IP

При превышении приходит 429 с заголовком Retry-After. Если интеграции штатно не хватает 60 запросов в минуту — это повод обсудить лимит, а не обходить его несколькими токенами.

Приём заказов

Главный метод для внешних витрин и агрегаторов. Заказ приходит без стола, кассира и открытой смены — у него отдельная точка входа, а не «подогнанный» обычный метод.

POST/integrations/orders

Требует право integration:write и заголовок Idempotency-Key в формате UUID.

Запрос
{
  "external_id": "SHOP-2026-000123",
  "source":      "lolotea",
  "type":        "delivery",
  "branch_id":   2,

  "customer": {
    "name":  "Иван Петров",
    "phone": "+998901234567",
    "email": "ivan@example.com"
  },

  "items": [
    {
      "product_id": 434,
      "quantity":   2,
      "unit_price": 22000,
      "notes":      "без сахара",
      "modifiers": [
        { "modifier_id": 89, "quantity": 1, "price_delta": 3000 }
      ]
    }
  ],

  "totals": {
    "subtotal":     44000,
    "discount":     0,
    "delivery_fee": 10000,
    "total":        54000,
    "currency":     "UZS"
  },

  "payment": {
    "method":                  "click",
    "status":                  "paid",
    "paid_amount":             54000,
    "external_transaction_id": "clk_88213"
  },

  "delivery": {
    "address":      "Ташкент, ул. Амира Темура, 5",
    "lat":          41.311081,
    "lng":          69.240562,
    "scheduled_at": "2026-08-07T18:30:00+05:00"
  },

  "notes": "Позвонить за 10 минут"
}
Ответ 201 — заказ создан
{
  "success": true,
  "data": {
    "order": {
      "id":           "973053a3-008f-47a7-ba3d-9598cc95dd4f",
      "order_number": "20260807-0002",
      "external_id":  "SHOP-2026-000123",
      "status":       "new",
      "type":         "delivery"
    }
  },
  "message": "Order accepted"
}

Обязательные поля

ПолеПравило
external_idстрока до 64 символов — ваш номер заказа, по нему считается уникальность
typedelivery или pickup
branch_idидентификатор филиала в RestoPOS
itemsминимум одна позиция
items[].product_idидентификатор товара; неизвестный отклоняет заказ целиком
items[].quantityцелое, минимум 1
items[].unit_priceцена за единицу без модификаторов
totals.subtotalсумма позиций
totals.totalитог к оплате
payment.methodcash, click или payme
payment.statuspaid или unpaid

Повторная отправка

Сеть рвётся, ответ теряется — повторяйте запрос с тем же Idempotency-Key. Тот же ключ с тем же телом вернёт исходный заказ и код 200 вместо 201, второй заказ на кухню не уйдёт. Тот же ключ с другим телом — это ошибка на вашей стороне, и она честно возвращается конфликтом.

Код ошибкиHTTPКогда
UNKNOWN_BRANCH422филиала нет в вашей организации
UNKNOWN_PRODUCTS422товары не найдены; список — в details.product_ids
UNKNOWN_MODIFIERS422модификаторы не найдены
IDEMPOTENCY_CONFLICT409ключ уже использован с другим телом запроса
Неизвестная позиция отклоняет заказ целиком

Частичный приём невозможен: половина корзины на кухне хуже честного отказа. Практически это означает, что ваш каталог разошёлся с нашим — синхронизируйте товары и повторите отправку.

Заказы

Работа с заказами в зале. Для приёма заказов извне используйте отдельный метод — этот рассчитан на кассира и открытую смену.

МетодНазначение
GET /ordersсписок с фильтрами
POST /ordersсоздать заказ
GET /orders/{id}карточка заказа
POST /orders/{id}/itemsдобавить позицию
PATCH /orders/{id}/items/{item}изменить позицию
DELETE /orders/{id}/items/{item}удалить позицию
POST /orders/{id}/send-to-kitchenотправить на кухню
POST /orders/{id}/discountприменить скидку
POST /orders/{id}/transferперенести на другой стол
POST /orders/{id}/closeзакрыть заказ
POST /orders/{id}/cancelотменить заказ
Заказ не редактируется через PUT

Изменение состава идёт через методы позиций, а смена состояния — через действия (close, cancel). Так каждое изменение проходит бизнес-правила: списание со склада, пересчёт сумм, проверку смены.

Клиенты

МетодНазначение
GET /customersсписок клиентов
GET /customers/searchпоиск по имени или телефону
POST /customersсоздать клиента
GET /customers/{id}/historyистория заказов
POST /customers/{id}/bonusначислить или списать бонусы

Склад

Учёт ведётся по ингредиентам, а не по готовым блюдам — остаток товара выводится из техкарты.

МетодНазначение
GET /warehouse/stockтекущие остатки
GET /warehouse/stock/lowпозиции ниже минимального остатка
POST /warehouse/stock/adjustкорректировка остатка
GET /warehouse/suppliesпоставки
POST /warehouse/suppliesсоздать поставку
POST /warehouse/supplies/{id}/receiveоприходовать поставку

Доставка

МетодНазначение
POST /delivery/zones/check-pointпопадает ли точка в зону доставки
GET /delivery/zonesзоны доставки
GET /delivery/couriersкурьеры
PATCH /delivery/couriers/{id}/locationобновить координаты курьера
PATCH /delivery/couriers/{id}/statusсменить статус курьера
GET /delivery/couriers/{id}/deliveriesактивные доставки курьера
GET /delivery/ordersзаказы на доставку
POST /delivery/orders/{id}/assignназначить курьера
POST /delivery/orders/{id}/pickupкурьер забрал заказ
POST /delivery/orders/{id}/deliverзаказ доставлен
POST /delivery/orders/{id}/rateоценка доставки

Проверка зоны полезна до создания заказа: она отвечает, обслуживается ли адрес, и позволяет не принимать заказ, который потом придётся отменять.

Отчёты и финансы

МетодНазначение
GET /reports/dashboardсводка за период
GET /reports/salesпродажи
GET /reports/productsпродажи по товарам
GET /reports/employeesпоказатели сотрудников
GET /reports/export/{type}выгрузка отчёта файлом
GET /finance/pnlотчёт о прибылях и убытках
GET /finance/cash-flowдвижение денежных средств
GET /finance/analyticsфинансовая аналитика
GET /cash-shifts/currentтекущая кассовая смена
GET /cash-shifts/{id}/reportотчёт по смене

Все методы ядра

Кроме перечисленного, в API есть разделы залов и столов, бронирований, KDS, сотрудников, принтеров и уведомлений — они рассчитаны на внутренние приложения и в этот справочник не вынесены.

Полная машинописная спецификация лежит в репозитории: docs/api_specification.yaml, формат OpenAPI 3.0.3 — её можно открыть в любом редакторе Swagger и сгенерировать клиент.

Нужен метод, которого здесь нет?

Напишите нам, что вы пытаетесь построить. Часто задача решается уже существующим методом, а если нет — это повод добавить его в публичную часть API, а не выдавать доступ к внутреннему.