Catalog Service — API Contract
Этот документ является единственным источником правды для API Catalog Service.
Бэкенд реализует контракт. Фронтенд потребляет контракт. Отклонения запрещены.
Содержание
Public API (Bearer JWT)
Категории
Модификаторы (справочник) (BR 1.8)
Модификаторы товара (BR 1.8.1)
Прейскуранты (BR 1.10)
Стоп-листы (BR 1.13)
Internal API (Service Token) (BR 1.16)
Вычисляемое меню
Полный снепшот для sync (BR 3.4)
Товары
GET /categories
Получить дерево категорий франшизы.
Параметр Значение Auth Bearer JWT (Franchise, Franchisee, Manager; Cashier — 403)
Response 200
{
"data" : [
{
"id" : "uuid" ,
"name" : "string" ,
"parent_id" : "uuid | null" ,
"display_order" : "integer" ,
"is_active" : "boolean" ,
"color" : "string | null" ,
"sort_type" : "manual | name_asc | name_desc | price_asc" ,
"is_available_mobile" : "boolean" ,
"is_available_website" : "boolean" ,
"is_available_aggregators" : "boolean" ,
"children" : [],
"created_at" : "datetime" ,
"updated_at" : "datetime"
}
]
}
Errors
Code HTTP Когда UNAUTHORIZED401 Невалидный JWT FORBIDDEN403 Роль Cashier
POST /categories
Создать категорию.
Параметр Значение Auth Bearer JWT (только Franchise) Content-Type application/json
Request Body
{
"name" : "string, required — название категории" ,
"parent_id" : "uuid, optional — ID родительской категории (null = корневая)" ,
"display_order" : "integer, optional — порядок (default 0)" ,
"is_active" : "boolean, optional — default true" ,
"color" : "string, optional — HEX-цвет (#RRGGBB)" ,
"sort_type" : "manual | name_asc | name_desc | price_asc, optional — default manual" ,
"is_available_mobile" : "boolean, optional — default true" ,
"is_available_website" : "boolean, optional — default true" ,
"is_available_aggregators" : "boolean, optional — default false"
}
Response 201
{
"data" : {
"id" : "uuid" ,
"name" : "string" ,
"parent_id" : "uuid | null" ,
"display_order" : "integer" ,
"is_active" : true ,
"color" : "string | null" ,
"sort_type" : "manual" ,
"is_available_mobile" : true ,
"is_available_website" : true ,
"is_available_aggregators" : false ,
"children" : [],
"created_at" : "datetime" ,
"updated_at" : "datetime"
}
}
Errors
Code HTTP Когда UNAUTHORIZED401 Невалидный JWT FORBIDDEN403 Роль не Franchise VALIDATION_ERROR400 Невалидные данные NAME_DUPLICATE409 Категория с таким названием уже существует PARENT_NOT_FOUND404 Родительская категория не найдена
PATCH /categories/{id}
Обновить категорию.
Параметр Значение Auth Bearer JWT (только Franchise) Content-Type application/json
Path Parameters
Param Type Required Description iduuid yes ID категории
Request Body
{
"name" : "string, optional" ,
"parent_id" : "uuid | null, optional" ,
"display_order" : "integer, optional" ,
"is_active" : "boolean, optional" ,
"cascade" : "boolean, optional — каскадная деактивация/активация дочерних" ,
"color" : "string, optional — HEX-цвет (#RRGGBB)" ,
"sort_type" : "manual | name_asc | name_desc | price_asc, optional" ,
"is_available_mobile" : "boolean, optional" ,
"is_available_website" : "boolean, optional" ,
"is_available_aggregators" : "boolean, optional"
}
Каскадная деактивация и активация (ADR-013)
is_active: false — автоматически деактивирует все дочерние категории
is_active: true, cascade: false — активируется только эта категория
is_active: true, cascade: true — активируется эта категория + все дочерние рекурсивно
Response 200
{
"data" : {
"id" : "uuid" ,
"name" : "string" ,
"parent_id" : "uuid | null" ,
"display_order" : "integer" ,
"is_active" : "boolean" ,
"color" : "string | null" ,
"sort_type" : "manual | name_asc | name_desc | price_asc" ,
"is_available_mobile" : "boolean" ,
"is_available_website" : "boolean" ,
"is_available_aggregators" : "boolean" ,
"children" : [],
"created_at" : "datetime" ,
"updated_at" : "datetime"
}
}
Errors
Code HTTP Когда UNAUTHORIZED401 Невалидный JWT FORBIDDEN403 Роль не Franchise CATEGORY_NOT_FOUND404 Категория не найдена NAME_DUPLICATE409 Категория с таким названием уже существует VALIDATION_ERROR400 Невалидные данные
DELETE /categories/{id}
Удалить категорию.
Параметр Значение Auth Bearer JWT (только Franchise)
Path Parameters
Param Type Required Description iduuid yes ID категории
Категория не должна содержать дочерних категорий (CATEGORY_HAS_CHILDREN)
Категория не должна содержать привязанных товаров (CATEGORY_HAS_PRODUCTS)
Response 204
Нет тела.
Errors
Code HTTP Когда UNAUTHORIZED401 Невалидный JWT FORBIDDEN403 Роль не Franchise CATEGORY_NOT_FOUND404 Категория не найдена CATEGORY_HAS_CHILDREN422 Есть дочерние категории CATEGORY_HAS_PRODUCTS422 Есть привязанные товары
GET /modifier-groups
(Добавлено в BR 1.8 )
Получить список групп модификаторов с пагинацией и поиском.
Параметр Значение Auth Bearer JWT (Franchise — все; Franchisee/Manager — активные; Cashier — 403)
Query Parameters
Param Type Required Description pageinteger no Номер страницы (default: 1) per_pageinteger no Записей на страницу (default: 20, max: 100) searchstring no Поиск по названию typestring no Фильтр: group / single statusstring no Фильтр: active / inactive deletedboolean no true — показать удалённыеexclude_structuralboolean no true — исключить группы, привязанные как structural к товарам (BUG-036)
Response 200
{
"data" : [
{
"id" : "uuid" ,
"name" : "string" ,
"type" : "group | single" ,
"min_amount" : "integer" ,
"max_amount" : "integer" ,
"status" : "active | inactive" ,
"option_count" : "integer"
}
],
"meta" : { "page" : "integer" , "per_page" : "integer" , "total" : "integer" }
}
Errors
Code HTTP Когда UNAUTHORIZED401 Невалидный JWT FORBIDDEN403 Cashier
GET /modifier-groups/{id}
(BR 1.8)
Получить детали группы модификаторов с опциями.
Параметр Значение Auth Bearer JWT (Franchise — любой; Franchisee/Manager — только активные; Cashier — 403)
Response 200
{
"data" : {
"id" : "uuid" ,
"name" : "string" ,
"type" : "group | single" ,
"min_amount" : "integer" ,
"max_amount" : "integer" ,
"status" : "active | inactive" ,
"options" : [
{
"id" : "uuid" ,
"name" : "string" ,
"min_amount" : "integer" ,
"max_amount" : "integer" ,
"default_amount" : "integer" ,
"free_quantity" : "integer" ,
"description" : "string | null" ,
"is_active" : "boolean" ,
"display_order" : "integer" ,
"sku_1c" : "string | null"
}
],
"created_at" : "datetime" ,
"updated_at" : "datetime"
}
}
(sku_1c добавлено в BR 1.17 ) — код номенклатуры 1С для опции. Заполняется только для опций structural-мода. См. 1С Общепит .
Errors
Code HTTP Когда UNAUTHORIZED401 FORBIDDEN403 MODIFIER_GROUP_NOT_FOUND404
POST /modifier-groups
(BR 1.8)
Создать группу модификаторов с опциями.
Параметр Значение Auth Bearer JWT (только Franchise) Content-Type application/json
Request Body
{
"name" : "string, required" ,
"type" : "string, required — group | single" ,
"min_amount" : "integer, optional — default 0" ,
"max_amount" : "integer, optional — default 1" ,
"options" : [
{
"name" : "string, required" ,
"min_amount" : "integer, optional — default 0" ,
"max_amount" : "integer, optional — default 1" ,
"default_amount" : "integer, optional — default 0" ,
"free_quantity" : "integer, optional — default 0" ,
"description" : "string, optional" ,
"is_active" : "boolean, optional — default true" ,
"display_order" : "integer, optional — auto" ,
"sku_1c" : "string, optional — код 1С (BR 1.17)"
}
]
}
Response 201
Аналогично GET /modifier-groups/{id} response.
Errors
Code HTTP Когда UNAUTHORIZED401 FORBIDDEN403 Не Franchise VALIDATION_ERROR400 Невалидные данные NAME_DUPLICATE409 Группа с таким названием уже существует DUPLICATE_SKU_1C_IN_GROUP422 Две опции в группе имеют одинаковый sku_1c (BR 1.17)
PATCH /modifier-groups/{id}
(BR 1.8)
Обновить группу модификаторов. Изменения применяются мгновенно.
Параметр Значение Auth Bearer JWT (только Franchise) Content-Type application/json
Request Body
Partial update. Опции заменяются целиком (если переданы).
{
"name" : "string, optional" ,
"type" : "string, optional" ,
"min_amount" : "integer, optional" ,
"max_amount" : "integer, optional" ,
"status" : "string, optional" ,
"options" : "array, optional — полная замена списка опций"
}
Response 200
Аналогично GET /modifier-groups/{id} response.
Errors
Code HTTP Когда UNAUTHORIZED401 FORBIDDEN403 MODIFIER_GROUP_NOT_FOUND404 VALIDATION_ERROR400 NAME_DUPLICATE409 STRUCTURAL_OPTION_MISSING_SKU_1C422 Опция в группе, которая привязана как structural к товару, не имеет sku_1c (BR 1.17) DUPLICATE_SKU_1C_IN_GROUP422 Две опции в группе имеют одинаковый sku_1c (BR 1.17)
DELETE /modifier-groups/{id}
(BR 1.8)
Soft delete группы модификаторов.
Параметр Значение Auth Bearer JWT (только Franchise)
Response 204
Errors
Code HTTP Когда UNAUTHORIZED401 FORBIDDEN403 MODIFIER_GROUP_NOT_FOUND404
POST /modifier-groups/{id}/restore
(BR 1.8)
Восстановить удалённую группу.
Параметр Значение Auth Bearer JWT (только Franchise)
Response 200
Аналогично GET /modifier-groups/{id} response.
Errors
Code HTTP Когда UNAUTHORIZED401 FORBIDDEN403 MODIFIER_GROUP_NOT_FOUND404 MODIFIER_GROUP_NOT_DELETED422 Группа не удалена NAME_DUPLICATE409 Имя занято
GET /modifier-groups/{id}/assignments
(MOD-05)
Получить список продуктов, к которым привязана группа модификаторов.
Параметр Значение Auth Bearer JWT (Franchise — все; Franchisee/Manager — 403)
Path Parameters
Param Type Required Description iduuid yes ID группы модификаторов
Response 200
{
"data" : [
{
"product_id" : "uuid" ,
"product_name" : "string" ,
"category_name" : "string | null" ,
"binding_type" : "structural | free"
}
]
}
Errors
Code HTTP Когда UNAUTHORIZED401 FORBIDDEN403 Не Franchise MODIFIER_GROUP_NOT_FOUND404
POST /modifier-groups/{id}/assignments
(MOD-04)
Массово назначить группу модификаторов на несколько продуктов. Уже привязанные продукты пропускаются.
Параметр Значение Auth Bearer JWT (только Franchise) Content-Type application/json
Request Body
{
"product_ids" : [ "uuid" , "uuid" ],
"binding_type" : "structural | free, optional, default: free"
}
Response 200
{
"data" : [
{
"product_id" : "uuid" ,
"product_name" : "string" ,
"category_name" : "string | null" ,
"binding_type" : "structural | free"
}
]
}
Errors
Code HTTP Когда UNAUTHORIZED401 FORBIDDEN403 Не Franchise MODIFIER_GROUP_NOT_FOUND404 VALIDATION_ERROR400 Пустой product_ids
POST /products/{id}/dependent-modifiers
(MOD-02)
Создать зависимый модификатор прямо на странице продукта. Создаёт группу типа single и автоматически привязывает к товару как structural.
Параметр Значение Auth Bearer JWT (только Franchise) Content-Type application/json
Path Parameters
Param Type Required Description iduuid yes ID товара
Request Body
{
"name" : "string, required — название группы" ,
"min_amount" : "integer, optional — default 0" ,
"max_amount" : "integer, optional — default 1" ,
"options" : [
{
"name" : "string, required" ,
"min_amount" : "integer, optional — default 0" ,
"max_amount" : "integer, optional — default 1" ,
"default_amount" : "integer, optional — default 0" ,
"free_quantity" : "integer, optional — default 0" ,
"description" : "string, optional" ,
"is_active" : "boolean, optional — default true" ,
"display_order" : "integer, optional — auto"
}
]
}
Response 201
Аналогично GET /products/{id}/modifiers response (один элемент).
Errors
Code HTTP Когда UNAUTHORIZED401 FORBIDDEN403 Не Franchise PRODUCT_NOT_FOUND404 VALIDATION_ERROR400 NAME_DUPLICATE409 Группа с таким названием уже существует
GET /products/{id}/modifiers
(BR 1.8.1, обновлено в BR 1.9.2)
Получить модификаторы товара.
Параметр Значение Auth Bearer JWT (Franchise — любой; Franchisee/Manager — только active товары)
Path Parameters
Param Type Required Description iduuid yes ID товара
Response 200
{
"data" : [
{
"modifier_group_id" : "uuid" ,
"binding_type" : "structural | free" ,
"name" : "string" ,
"type" : "group | single" ,
"min_amount" : "integer" ,
"max_amount" : "integer" ,
"override_min_amount" : "integer | null" ,
"override_max_amount" : "integer | null" ,
"options" : [
{
"id" : "uuid" ,
"name" : "string" ,
"min_amount" : "integer" ,
"max_amount" : "integer" ,
"default_amount" : "integer" ,
"free_quantity" : "integer" ,
"description" : "string | null" ,
"is_active" : "boolean" ,
"display_order" : "integer"
}
]
}
]
}
Errors
Code HTTP Когда UNAUTHORIZED401 FORBIDDEN403 PRODUCT_NOT_FOUND404
POST /products/{id}/modifiers
(BR 1.8.1, обновлено в BR 1.9.2)
Привязать группу модификаторов к товару. Изменения применяются мгновенно.
Параметр Значение Auth Bearer JWT (только Franchise) Content-Type application/json
Request Body
{
"modifier_group_id" : "uuid, required" ,
"binding_type" : "structural | free, optional, default: free — тип привязки (BR 1.9.2)" ,
"override_min_amount" : "integer, optional — per-product override" ,
"override_max_amount" : "integer, optional — per-product override"
}
Response 201
{
"data" : {
"modifier_group_id" : "uuid" ,
"binding_type" : "structural | free" ,
"name" : "string" ,
"type" : "group | single" ,
"min_amount" : "integer" ,
"max_amount" : "integer" ,
"override_min_amount" : "integer | null" ,
"override_max_amount" : "integer | null"
}
}
Errors
Code HTTP Когда UNAUTHORIZED401 FORBIDDEN403 Роль не Franchise PRODUCT_NOT_FOUND404 MODIFIER_GROUP_NOT_FOUND404 MODIFIER_ALREADY_ATTACHED409 Группа уже привязана к этому товару STRUCTURAL_MODIFIER_MIN_REQUIRED422 binding_type=structural, но override_min_amount < 1 (BR 1.9.2) STRUCTURAL_OPTION_MISSING_SKU_1C422 binding_type=structural, но хотя бы одна опция группы не имеет sku_1c (BR 1.17)
DELETE /products/{id}/modifiers/{groupId}
(BR 1.8.1)
Отвязать группу модификаторов от товара.
Параметр Значение Auth Bearer JWT (только Franchise)
Path Parameters
Param Type Required Description iduuid yes ID товара groupIduuid yes ID группы модификаторов
Response 204
Errors
Code HTTP Когда UNAUTHORIZED401 FORBIDDEN403 PRODUCT_NOT_FOUND404 MODIFIER_NOT_ATTACHED404 Группа не привязана к этому товару
PATCH /products/{id}/modifiers/{groupId}
(BR 1.8.1, обновлено в BR 1.9.2)
Обновить привязку: override min/max или тип привязки.
Параметр Значение Auth Bearer JWT (только Franchise) Content-Type application/json
Request Body
{
"binding_type" : "structural | free, optional — тип привязки (BR 1.9.2)" ,
"override_min_amount" : "integer | null, optional" ,
"override_max_amount" : "integer | null, optional"
}
Response 200
Аналогично POST .../modifiers response.
Errors
Code HTTP Когда UNAUTHORIZED401 FORBIDDEN403 PRODUCT_NOT_FOUND404 MODIFIER_NOT_ATTACHED404 STRUCTURAL_MODIFIER_MIN_REQUIRED422 Смена на binding_type=structural, но override_min_amount < 1 STRUCTURAL_OPTION_MISSING_SKU_1C422 Смена на binding_type=structural, но опции группы не имеют sku_1c (BR 1.17)
GET /price-lists
(BR 1.10)
Список прейскурантов франшизы.
Параметр Значение Auth Bearer JWT (Franchise — все; Franchisee/Manager — только свой assigned; Cashier — 403)
Query Parameters
Param Type Required Description pageinteger no Номер страницы (default: 1) per_pageinteger no Записей на страницу (default: 20, max: 100) searchstring no Поиск по названию
Response 200
{
"data" : [
{
"id" : "uuid" ,
"name" : "string" ,
"is_default" : "boolean" ,
"status" : "active | inactive" ,
"store_count" : "integer — сколько ТТ назначено" ,
"created_at" : "datetime"
}
],
"meta" : {
"page" : "integer" ,
"per_page" : "integer" ,
"total" : "integer"
}
}
Errors
Code HTTP Когда UNAUTHORIZED401 Невалидный JWT FORBIDDEN403 Cashier
GET /price-lists/{id}
(BR 1.10)
Получить детали прейскуранта с привязанными ТТ.
Параметр Значение Auth Bearer JWT (Franchise — любой; Franchisee/Manager — только если назначен на их ТТ; Cashier — 403)
Path Parameters
Param Type Required Description iduuid yes ID прейскуранта
Response 200
{
"data" : {
"id" : "uuid" ,
"name" : "string" ,
"is_default" : "boolean" ,
"status" : "active | inactive" ,
"stores" : [
{
"id" : "uuid" ,
"name" : "string"
}
],
"created_at" : "datetime" ,
"updated_at" : "datetime"
}
}
Errors
Code HTTP Когда UNAUTHORIZED401 Невалидный JWT FORBIDDEN403 Cashier или прейскурант не назначен на ТТ пользователя PRICE_LIST_NOT_FOUND404 Прейскурант не найден
POST /price-lists
(BR 1.10)
Создать прейскурант. Автоматически заполняется всеми активными товарами и опциями модификаторов — цены копируются из дефолтного прейскуранта (или 0₽ если дефолтного нет). (Исправлено в BUG-001 )
Параметр Значение Auth Bearer JWT (только Franchise) Content-Type application/json
Request Body
{
"name" : "string, required — название прейскуранта" ,
"is_default" : "boolean, optional — default false"
}
Response 201
{
"data" : {
"id" : "uuid" ,
"name" : "string" ,
"is_default" : "boolean" ,
"status" : "active" ,
"stores" : [],
"created_at" : "datetime" ,
"updated_at" : "datetime"
}
}
Errors
Code HTTP Когда UNAUTHORIZED401 Невалидный JWT FORBIDDEN403 Роль не Franchise VALIDATION_ERROR400 Невалидные данные NAME_DUPLICATE409 Прейскурант с таким названием уже существует
PATCH /price-lists/{id}
(BR 1.10)
Обновить прейскурант (название, статус, is_default).
Параметр Значение Auth Bearer JWT (только Franchise) Content-Type application/json
Path Parameters
Param Type Required Description iduuid yes ID прейскуранта
Request Body
{
"name" : "string, optional" ,
"is_default" : "boolean, optional" ,
"status" : "string, optional — active | inactive"
}
Response 200
{
"data" : {
"id" : "uuid" ,
"name" : "string" ,
"is_default" : "boolean" ,
"status" : "active | inactive" ,
"stores" : [
{
"id" : "uuid" ,
"name" : "string"
}
],
"created_at" : "datetime" ,
"updated_at" : "datetime"
}
}
Errors
Code HTTP Когда UNAUTHORIZED401 Невалидный JWT FORBIDDEN403 Роль не Franchise PRICE_LIST_NOT_FOUND404 Прейскурант не найден NAME_DUPLICATE409 Прейскурант с таким названием уже существует
DELETE /price-lists/{id}
(BR 1.10)
Удалить прейскурант. Нельзя удалить дефолтный прейскурант или назначенный на ТТ.
Параметр Значение Auth Bearer JWT (только Franchise)
Path Parameters
Param Type Required Description iduuid yes ID прейскуранта
Response 204
Нет тела.
Errors
Code HTTP Когда UNAUTHORIZED401 Невалидный JWT FORBIDDEN403 Роль не Franchise PRICE_LIST_NOT_FOUND404 Прейскурант не найден CANNOT_DELETE_DEFAULT422 Нельзя удалить дефолтный прейскурант PRICE_LIST_ASSIGNED_TO_STORES422 Прейскурант назначен на ТТ (details: список названий ТТ)
GET /price-lists/{id}/items
(BR 1.10)
Получить цены прейскуранта (товары + модификаторы). Цены редактируются напрямую, без версионирования.
Параметр Значение Auth Bearer JWT (только Franchise)
Path Parameters
Param Type Required Description iduuid yes ID прейскуранта
Query Parameters
Param Type Required Description searchstring no Поиск по названию товара / опции
Response 200
{
"data" : {
"price_list_id" : "uuid" ,
"items" : [
{
"product_id" : "uuid" ,
"product_name" : "string" ,
"price" : "decimal"
}
],
"modifier_items" : [
{
"modifier_option_id" : "uuid" ,
"modifier_option_name" : "string" ,
"modifier_group_name" : "string" ,
"price" : "decimal"
}
]
}
}
Errors
Code HTTP Когда UNAUTHORIZED401 Невалидный JWT FORBIDDEN403 Роль не Franchise PRICE_LIST_NOT_FOUND404 Прейскурант не найден
PATCH /price-lists/{id}/items
(BR 1.10)
Пакетное обновление цен товаров в прейскуранте. Изменения применяются мгновенно.
Параметр Значение Auth Bearer JWT (только Franchise) Content-Type application/json
Path Parameters
Param Type Required Description iduuid yes ID прейскуранта
Request Body
{
"items" : [
{
"product_id" : "uuid, required" ,
"price" : "decimal, required — >= 0"
}
]
}
Response 200
{
"data" : {
"updated_count" : "integer"
}
}
Errors
Code HTTP Когда UNAUTHORIZED401 Невалидный JWT FORBIDDEN403 Роль не Franchise PRICE_LIST_NOT_FOUND404 Прейскурант не найден VALIDATION_ERROR400 Невалидные данные (price < 0)
PATCH /price-lists/{id}/modifier-items
(BR 1.10)
Пакетное обновление цен опций модификаторов в прейскуранте. Изменения применяются мгновенно.
Параметр Значение Auth Bearer JWT (только Franchise) Content-Type application/json
Path Parameters
Param Type Required Description iduuid yes ID прейскуранта
Request Body
{
"items" : [
{
"modifier_option_id" : "uuid, required" ,
"price" : "decimal, required — >= 0"
}
]
}
Response 200
{
"data" : {
"updated_count" : "integer"
}
}
Errors
Code HTTP Когда UNAUTHORIZED401 Невалидный JWT FORBIDDEN403 Роль не Franchise PRICE_LIST_NOT_FOUND404 Прейскурант не найден VALIDATION_ERROR400 Невалидные данные (price < 0)
GET /products
Получить список товаров с пагинацией, поиском и фильтрами.
Параметр Значение Auth Bearer JWT (Franchise, Franchisee, Manager; Cashier — 403)
Query Parameters
Param Type Required Description pageinteger no Номер страницы (default: 1) per_pageinteger no Записей на страницу (default: 20, max: 100) searchstring no Поиск по названию товара typestring no Фильтр: dish / good (BR 1.11: type=ingredient removed — ingredients moved to Warehouse Service) statusstring no Фильтр: active / inactive deletedboolean no true — показать только удалённые товары (для вкладки “Удалённые”, только Franchise)category_iduuid no Фильтр по категории sortstring no Сортировка: name_asc (default) / name_desc / created_at_desc / sort_order_asc (BR 2.1)
Response 200
{
"data" : [
{
"id" : "uuid" ,
"name" : "string" ,
"type" : "dish | good" ,
"category_id" : "uuid | null" ,
"category_name" : "string | null" ,
"unit_of_measure" : "string" ,
"status" : "active | inactive" ,
"sku" : "string | null" ,
"sort_order" : "integer" ,
"is_alcohol" : "boolean" ,
"image_url" : "string | null"
}
],
"meta" : {
"page" : "integer" ,
"per_page" : "integer" ,
"total" : "integer"
}
}
Ролевой доступ
Franchise : все товары франшизы — это справочник
Franchisee : активные товары
Manager : активные товары
Cashier : 403 FORBIDDEN
Errors
Code HTTP Когда UNAUTHORIZED401 Невалидный JWT FORBIDDEN403 Роль Cashier
GET /products/{id}
Получить детали товара.
Параметр Значение Auth Bearer JWT (Franchise — любой товар; Franchisee/Manager — активные товары; Cashier — 403)
Path Parameters
Param Type Required Description iduuid yes ID товара
Response 200
{
"data" : {
"id" : "uuid" ,
"name" : "string" ,
"description" : "string | null" ,
"type" : "dish | good" ,
"category_id" : "uuid | null" ,
"category_name" : "string | null" ,
"unit_of_measure" : "string" ,
"status" : "active | inactive" ,
"sku" : "string | null" ,
"barcode" : "string | null" ,
"sort_order" : "integer" ,
"gross_weight" : "decimal | null" ,
"net_weight" : "decimal | null" ,
"kcal" : "decimal | null" ,
"protein" : "decimal | null" ,
"fat" : "decimal | null" ,
"carbs" : "decimal | null" ,
"assembly_time" : "integer | null" ,
"color" : "string | null" ,
"image_url" : "string | null" ,
"is_open_price" : "boolean" ,
"is_by_weight" : "boolean" ,
"is_exclude_from_promo" : "boolean" ,
"is_manual_discount_banned" : "boolean" ,
"is_admin_only" : "boolean" ,
"is_alcohol" : "boolean" ,
"is_tobacco" : "boolean" ,
"is_sugary_drink" : "boolean" ,
"is_marked" : "boolean" ,
"available_in_all_stores" : "boolean" ,
"store_ids" : [ "uuid" ] ,
"modifiers" : [],
"deleted_at" : "datetime | null" ,
"created_at" : "datetime" ,
"updated_at" : "datetime"
}
}
(BR 1.10) Цена определяется прейскурантом.
(BR 2.1) store_ids — null когда available_in_all_stores = true, иначе список UUID ТТ.
Errors
Code HTTP Когда UNAUTHORIZED401 Невалидный JWT FORBIDDEN403 Cashier PRODUCT_NOT_FOUND404 Товар не найден или удалён
POST /products
Создать товар в справочнике франшизы.
Параметр Значение Auth Bearer JWT (только Franchise) Content-Type application/json
Request Body
{
"name" : "string, required — название товара (уникальное в рамках франшизы)" ,
"description" : "string, optional — описание" ,
"type" : "string, required — dish | good" ,
"category_id" : "uuid, optional — FK → categories.id" ,
"unit_of_measure" : "string, required — piece | kg | g | liter | ml | portion" ,
// BR 2.1: optional extended fields
"sku" : "string, optional — артикул" ,
"barcode" : "string, optional — штрихкод" ,
"sort_order" : "integer, optional — порядок сортировки (default: 0)" ,
"gross_weight" : "decimal, optional — вес брутто, кг" ,
"net_weight" : "decimal, optional — вес нетто, кг" ,
"kcal" : "decimal, optional — калорийность на 100г" ,
"protein" : "decimal, optional — белки на 100г" ,
"fat" : "decimal, optional — жиры на 100г" ,
"carbs" : "decimal, optional — углеводы на 100г" ,
"assembly_time" : "integer, optional — время приготовления, мин" ,
"color" : "string, optional — цвет POS (hex, e.g. '#FF5733')" ,
"is_open_price" : "boolean, optional — свободная цена (default: false)" ,
"is_by_weight" : "boolean, optional — продажа на развес (default: false)" ,
"is_exclude_from_promo" : "boolean, optional — исключить из акций (default: false)" ,
"is_manual_discount_banned" : "boolean, optional — запрет ручных скидок (default: false)" ,
"is_admin_only" : "boolean, optional — только администратор (default: false)" ,
"is_alcohol" : "boolean, optional — алкоголь (default: false)" ,
"is_tobacco" : "boolean, optional — табак (default: false)" ,
"is_sugary_drink" : "boolean, optional — сахаросодержащий напиток (default: false)" ,
"is_marked" : "boolean, optional — маркированный товар ЧЗ (default: false)" ,
"available_in_all_stores" : "boolean, optional — доступен во всех ТТ (default: true)" ,
"vat_rate" : "string, optional — ставка НДС для 54-ФЗ (default: vat20) — BR 3.3" ,
"payment_subject" : "string, optional — предмет расчёта (default: goods) — BR 3.3" ,
"payment_type" : "string, optional — способ расчёта (default: full) — BR 3.3"
}
Допустимые значения: piece, kg, g, liter, ml, portion. Маппинг на UI: piece=шт, kg=кг, g=г, liter=л, ml=мл, portion=порция.
vat_rate enum: none / vat0 / vat10 / vat20 / vat110 / vat120.
payment_subject enum: goods / service / work / excise / job / payment / agency / composite / another.
payment_type enum: full / prepay / advance / partial_prepay / credit / credit_pay / partial.
Передаются Paykeeper Adapter при формировании инвойса (fiscal_cart по 54-ФЗ). См. бизнес-спека PayKeeper .
Response 201
Полный ProductResponse (аналогично GET /products/{id} ).
{
"data" : {
"id" : "uuid" ,
"name" : "string" ,
"description" : "string | null" ,
"type" : "dish | good" ,
"category_id" : "uuid | null" ,
"category_name" : "string | null" ,
"unit_of_measure" : "string" ,
"status" : "active" ,
"sku" : "string | null" ,
"barcode" : "string | null" ,
"sort_order" : "integer" ,
"gross_weight" : "decimal | null" ,
"net_weight" : "decimal | null" ,
"kcal" : "decimal | null" ,
"protein" : "decimal | null" ,
"fat" : "decimal | null" ,
"carbs" : "decimal | null" ,
"assembly_time" : "integer | null" ,
"color" : "string | null" ,
"image_url" : "string | null" ,
"is_open_price" : "boolean" ,
"is_by_weight" : "boolean" ,
"is_exclude_from_promo" : "boolean" ,
"is_manual_discount_banned" : "boolean" ,
"is_admin_only" : "boolean" ,
"is_alcohol" : "boolean" ,
"is_tobacco" : "boolean" ,
"is_sugary_drink" : "boolean" ,
"is_marked" : "boolean" ,
"available_in_all_stores" : "boolean" ,
"vat_rate" : "string" ,
"payment_subject" : "string" ,
"payment_type" : "string" ,
"store_ids" : [ "uuid" ],
"modifiers" : [],
"deleted_at" : "null" ,
"created_at" : "datetime" ,
"updated_at" : "datetime"
}
}
Errors
Code HTTP Когда UNAUTHORIZED401 Невалидный JWT FORBIDDEN403 Роль не Franchise VALIDATION_ERROR400 Невалидные данные (пустое название, невалидный тип) NAME_DUPLICATE409 Товар с таким названием уже существует в каталоге франшизы
PATCH /products/{id}
Обновить товар. Изменения применяются мгновенно.
Параметр Значение Auth Bearer JWT (только Franchise) Content-Type application/json
Path Parameters
Param Type Required Description iduuid yes ID товара
Request Body
Partial update — отправляются только изменяемые поля.
{
"name" : "string, optional" ,
"description" : "string | null, optional" ,
"type" : "string, optional — dish | good" ,
"category_id" : "uuid | null, optional — FK → categories.id" ,
"unit_of_measure" : "string, optional — piece | kg | g | liter | ml | portion" ,
"status" : "string, optional — active | inactive" ,
// BR 2.1: optional extended fields
"sku" : "string | null, optional" ,
"barcode" : "string | null, optional" ,
"sort_order" : "integer, optional" ,
"gross_weight" : "decimal | null, optional" ,
"net_weight" : "decimal | null, optional" ,
"kcal" : "decimal | null, optional" ,
"protein" : "decimal | null, optional" ,
"fat" : "decimal | null, optional" ,
"carbs" : "decimal | null, optional" ,
"assembly_time" : "integer | null, optional" ,
"color" : "string | null, optional" ,
"is_open_price" : "boolean, optional" ,
"is_by_weight" : "boolean, optional" ,
"is_exclude_from_promo" : "boolean, optional" ,
"is_manual_discount_banned" : "boolean, optional" ,
"is_admin_only" : "boolean, optional" ,
"is_alcohol" : "boolean, optional" ,
"is_tobacco" : "boolean, optional" ,
"is_sugary_drink" : "boolean, optional" ,
"is_marked" : "boolean, optional" ,
"available_in_all_stores" : "boolean, optional"
}
Response 200
Полный ProductResponse (аналогично GET /products/{id} ).
{
"data" : {
"id" : "uuid" ,
"name" : "string" ,
"description" : "string | null" ,
"type" : "dish | good" ,
"category_id" : "uuid | null" ,
"category_name" : "string | null" ,
"unit_of_measure" : "string" ,
"status" : "active | inactive" ,
"sku" : "string | null" ,
"barcode" : "string | null" ,
"sort_order" : "integer" ,
"gross_weight" : "decimal | null" ,
"net_weight" : "decimal | null" ,
"kcal" : "decimal | null" ,
"protein" : "decimal | null" ,
"fat" : "decimal | null" ,
"carbs" : "decimal | null" ,
"assembly_time" : "integer | null" ,
"color" : "string | null" ,
"image_url" : "string | null" ,
"is_open_price" : "boolean" ,
"is_by_weight" : "boolean" ,
"is_exclude_from_promo" : "boolean" ,
"is_manual_discount_banned" : "boolean" ,
"is_admin_only" : "boolean" ,
"is_alcohol" : "boolean" ,
"is_tobacco" : "boolean" ,
"is_sugary_drink" : "boolean" ,
"is_marked" : "boolean" ,
"available_in_all_stores" : "boolean" ,
"vat_rate" : "string" ,
"payment_subject" : "string" ,
"payment_type" : "string" ,
"store_ids" : [ "uuid" ],
"modifiers" : [],
"deleted_at" : "datetime | null" ,
"created_at" : "datetime" ,
"updated_at" : "datetime"
}
}
Errors
Code HTTP Когда UNAUTHORIZED401 Невалидный JWT FORBIDDEN403 Роль не Franchise PRODUCT_NOT_FOUND404 Товар не найден или удалён VALIDATION_ERROR400 Невалидные данные NAME_DUPLICATE409 Товар с таким названием уже существует в каталоге франшизы
DELETE /products/{id}
Удалить товар (soft delete). Устанавливает deleted_at, товар скрывается из общего списка.
Параметр Значение Auth Bearer JWT (только Franchise)
Path Parameters
Param Type Required Description iduuid yes ID товара
Response 204
Нет тела.
Errors
Code HTTP Когда UNAUTHORIZED401 Невалидный JWT FORBIDDEN403 Роль не Franchise PRODUCT_NOT_FOUND404 Товар не найден или уже удалён
POST /products/{id}/restore
Восстановить удалённый товар. Убирает deleted_at.
Параметр Значение Auth Bearer JWT (только Franchise)
Path Parameters
Param Type Required Description iduuid yes ID товара
Response 200
Полный ProductResponse (аналогично GET /products/{id} ).
{
"data" : {
"id" : "uuid" ,
"name" : "string" ,
"description" : "string | null" ,
"type" : "dish | good" ,
"category_id" : "uuid | null" ,
"category_name" : "string | null" ,
"unit_of_measure" : "string" ,
"status" : "active | inactive" ,
"sku" : "string | null" ,
"barcode" : "string | null" ,
"sort_order" : "integer" ,
"gross_weight" : "decimal | null" ,
"net_weight" : "decimal | null" ,
"kcal" : "decimal | null" ,
"protein" : "decimal | null" ,
"fat" : "decimal | null" ,
"carbs" : "decimal | null" ,
"assembly_time" : "integer | null" ,
"color" : "string | null" ,
"image_url" : "string | null" ,
"is_open_price" : "boolean" ,
"is_by_weight" : "boolean" ,
"is_exclude_from_promo" : "boolean" ,
"is_manual_discount_banned" : "boolean" ,
"is_admin_only" : "boolean" ,
"is_alcohol" : "boolean" ,
"is_tobacco" : "boolean" ,
"is_sugary_drink" : "boolean" ,
"is_marked" : "boolean" ,
"available_in_all_stores" : "boolean" ,
"vat_rate" : "string" ,
"payment_subject" : "string" ,
"payment_type" : "string" ,
"store_ids" : [ "uuid" ],
"modifiers" : [],
"deleted_at" : "null" ,
"created_at" : "datetime" ,
"updated_at" : "datetime"
}
}
Errors
Code HTTP Когда UNAUTHORIZED401 Невалидный JWT FORBIDDEN403 Роль не Franchise PRODUCT_NOT_FOUND404 Товар не найден PRODUCT_NOT_DELETED422 Товар не удалён (нельзя восстановить активный товар) NAME_DUPLICATE409 Товар с таким названием уже существует (другой товар занял имя после удаления)
PATCH /products/{id}/stores
(BR 2.1) Задать список ТТ, в которых доступен товар. Если передать пустой массив — товар доступен во всех ТТ (available_in_all_stores = true).
Параметр Значение Auth Bearer JWT (только Franchise) Content-Type application/json
Path Parameters
Param Type Required Description iduuid yes ID товара
Request Body
{
"store_ids" : [ "uuid" , "uuid" ]
}
Поле Тип Required Описание store_idsuuid[] yes Список store_id. Пустой массив = доступен везде
Response 200
Возвращает полный объект товара (см. GET /products/{id}).
Errors
Code HTTP Когда UNAUTHORIZED401 Невалидный JWT FORBIDDEN403 Роль не Franchise PRODUCT_NOT_FOUND404 Товар не найден
POST /products/{id}/image
(BR 2.1) Загрузить или заменить изображение товара в S3. Возвращает публичный URL.
Параметр Значение Auth Bearer JWT (только Franchise) Content-Type multipart/form-data
Path Parameters
Param Type Required Description iduuid yes ID товара
Request Body (form-data)
Поле Тип Required Описание fileFile yes Файл изображения (jpg, png, webp)
Response 200
{
"data" : {
"image_url" : "https://s3.example.com/erp-catalog/products/{id}/image.jpg"
}
}
Errors
Code HTTP Когда UNAUTHORIZED401 Невалидный JWT FORBIDDEN403 Роль не Franchise PRODUCT_NOT_FOUND404 Товар не найден
DELETE /products/{id}/image
(BR 2.1) Удалить изображение товара из S3 и очистить поле image_url.
Параметр Значение Auth Bearer JWT (только Franchise)
Path Parameters
Param Type Required Description iduuid yes ID товара
Response 204
Нет тела.
Errors
Code HTTP Когда UNAUTHORIZED401 Невалидный JWT FORBIDDEN403 Роль не Franchise PRODUCT_NOT_FOUND404 Товар не найден
GET /stop-lists/stores/{storeId}
(BR 1.13)
Получить стоп-лист торговой точки — все остановленные товары и категории.
Параметр Значение Auth Bearer JWT (Franchise — любая ТТ; Franchisee — свои ТТ; Manager — своя ТТ; Cashier — read-only)
Path Parameters
Param Type Required Description storeIduuid yes ID торговой точки
Response 200
{
"data" : {
"products" : [
{
"product_id" : "uuid" ,
"product_name" : "string" ,
"reason" : "string | null" ,
"stopped_by" : "uuid" ,
"created_at" : "datetime"
}
],
"categories" : [
{
"category_id" : "uuid" ,
"category_name" : "string" ,
"reason" : "string | null" ,
"stopped_by" : "uuid" ,
"created_at" : "datetime"
}
]
}
}
Errors
Code HTTP Когда UNAUTHORIZED401 Невалидный JWT STORE_ACCESS_DENIED403 Нет доступа к этой ТТ
POST /stop-lists/stores/{storeId}/products
(BR 1.13)
Остановить товар на ТТ (добавить в стоп-лист).
Параметр Значение Auth Bearer JWT (Franchise — любая ТТ; Franchisee — свои ТТ; Manager — своя ТТ; Cashier — 403) Content-Type application/json
Path Parameters
Param Type Required Description storeIduuid yes ID торговой точки
Request Body
{
"product_id" : "uuid, required — ID товара" ,
"reason" : "string, optional — причина остановки"
}
Response 201
{
"data" : {
"product_id" : "uuid" ,
"product_name" : "string" ,
"reason" : "string | null" ,
"stopped_by" : "uuid" ,
"created_at" : "datetime"
}
}
Errors
Code HTTP Когда UNAUTHORIZED401 Невалидный JWT STORE_ACCESS_DENIED403 Нет доступа к этой ТТ PRODUCT_NOT_FOUND404 Товар не найден PRODUCT_ALREADY_STOPPED409 Товар уже в стоп-листе на этой ТТ
DELETE /stop-lists/stores/{storeId}/products/{productId}
(BR 1.13)
Снять стоп с товара (убрать из стоп-листа).
Параметр Значение Auth Bearer JWT (Franchise — любая ТТ; Franchisee — свои ТТ; Manager — своя ТТ; Cashier — 403)
Path Parameters
Param Type Required Description storeIduuid yes ID торговой точки productIduuid yes ID товара
Response 204
Нет тела.
Errors
Code HTTP Когда UNAUTHORIZED401 Невалидный JWT STORE_ACCESS_DENIED403 Нет доступа к этой ТТ PRODUCT_NOT_STOPPED404 Товар не в стоп-листе
POST /stop-lists/stores/{storeId}/categories
(BR 1.13)
Остановить категорию на ТТ (добавить в стоп-лист). Блокирует все товары этой категории.
Параметр Значение Auth Bearer JWT (Franchise — любая ТТ; Franchisee — свои ТТ; Manager — своя ТТ; Cashier — 403) Content-Type application/json
Path Parameters
Param Type Required Description storeIduuid yes ID торговой точки
Request Body
{
"category_id" : "uuid, required — ID категории" ,
"reason" : "string, optional — причина остановки"
}
Response 201
{
"data" : {
"category_id" : "uuid" ,
"category_name" : "string" ,
"reason" : "string | null" ,
"stopped_by" : "uuid" ,
"created_at" : "datetime"
}
}
Errors
Code HTTP Когда UNAUTHORIZED401 Невалидный JWT STORE_ACCESS_DENIED403 Нет доступа к этой ТТ CATEGORY_NOT_FOUND404 Категория не найдена CATEGORY_ALREADY_STOPPED409 Категория уже в стоп-листе на этой ТТ
DELETE /stop-lists/stores/{storeId}/categories/{categoryId}
(BR 1.13)
Снять стоп с категории (убрать из стоп-листа).
Параметр Значение Auth Bearer JWT (Franchise — любая ТТ; Franchisee — свои ТТ; Manager — своя ТТ; Cashier — 403)
Path Parameters
Param Type Required Description storeIduuid yes ID торговой точки categoryIduuid yes ID категории
Response 204
Нет тела.
Errors
Code HTTP Когда UNAUTHORIZED401 Невалидный JWT STORE_ACCESS_DENIED403 Нет доступа к этой ТТ CATEGORY_NOT_STOPPED404 Категория не в стоп-листе
GET /stop-lists/stores/{storeId}/check/{productId}
(BR 1.13)
Проверить доступность товара на ТТ (учитывает стоп-листы по товару и по категории). Используется для POS и вычисляемого меню.
Параметр Значение Auth Bearer JWT (все роли)
Path Parameters
Param Type Required Description storeIduuid yes ID торговой точки productIduuid yes ID товара
Response 200
{
"data" : {
"available" : "boolean" ,
"reason" : "string | null — причина блокировки (если есть)" ,
"stopped_by" : "product | category | null — источник блокировки"
}
}
Errors
Code HTTP Когда UNAUTHORIZED401 Невалидный JWT PRODUCT_NOT_FOUND404 Товар не найден
Вычисляемое меню для ТТ (BR 1.16) . Возвращает категории, товары с ценами и модификаторами в одном запросе. При передаче store_id — фильтрует по стоп-листам.
Параметр Значение Auth Service token (X-Service-Token)
Query Parameters
Параметр Тип Обязательный Описание franchise_iduuid Да Tenant scope price_list_iduuid Нет Прейскурант; если не передан — используется дефолтный франшизы store_iduuid Нет ТТ для фильтрации по стоп-листам
Response 200
{
"data" : {
"price_list_id" : "uuid | null" ,
"categories" : [
{
"id" : "uuid" ,
"name" : "string" ,
"parent_id" : "uuid | null" ,
"sort_order" : "integer"
}
],
"products" : [
{
"id" : "uuid" ,
"name" : "string" ,
"category_id" : "uuid" ,
"image_url" : "string | null" ,
"is_open_price" : "boolean" ,
"price" : "decimal | null" ,
"modifiers" : [
{
"group_id" : "uuid" ,
"group_name" : "string" ,
"binding_type" : "string — free | structural" ,
"min" : "integer — effective min (override или group default)" ,
"max" : "integer — effective max (override или group default)" ,
"options" : [
{
"id" : "uuid" ,
"name" : "string" ,
"price" : "decimal | null — цена из прейскуранта (только для free)"
}
]
}
]
}
]
}
}
Фильтрация по стоп-листам
Когда передан store_id:
Категории из стоп-листа ТТ исключаются
Товары из стоп-листа ТТ исключаются
Товары, чья категория в стоп-листе, тоже исключаются
Без store_id — возвращается полный каталог без фильтрации.
binding_type: "structural" — опции без цены (вариации товара, цена в product price)
binding_type: "free" — опции с ценой из прейскуранта (price_list_modifier_items)
min/max — effective значения: если в product_modifiers есть override — он приоритетнее
Кухонные станции (BR 2.5)
Секция добавлена в BR 2.5 . Бизнес-спека: Кухонные станции .
GET /kitchen-stations
Список станций франшизы.
Параметр Значение Auth Bearer JWT с permission catalog.read
Query Parameters
Param Type Required Description include_deletedbool no По умолчанию false. True — вернуть и soft-deleted
Response 200
{
"data" : [
{
"id" : "uuid" ,
"name" : "Горячая кухня" ,
"description" : "string | null" ,
"yellow_threshold_minutes" : 5 ,
"red_threshold_minutes" : 0 ,
"product_count" : 42 ,
"created_at" : "datetime" ,
"updated_at" : "datetime"
}
]
}
product_count — количество активных товаров, ссылающихся на станцию.
yellow_threshold_minutes / red_threshold_minutes — пороги цвета на KDS-карточке (см. Кухонные станции ). (Добавлено в BR 5.1 )
POST /kitchen-stations
Создать станцию.
Параметр Значение Auth Bearer JWT с catalog.edit Content-Type application/json
Request Body
{
"name" : "Горячая кухня" ,
"description" : "Плита, вок, гриль — станция для горячих блюд" ,
"yellow_threshold_minutes" : 5 ,
"red_threshold_minutes" : 0
}
Field Type Required Description namestring yes Уникально per franchise (регистронезависимо), max 50 символов descriptionstring no yellow_threshold_minutesint no Default 5. Порог жёлтой зоны на KDS. (BR 5.1) red_threshold_minutesint no Default 0. Порог просрочки. (BR 5.1)
Response 201
{
"data" : {
"id" : "uuid" ,
"name" : "Горячая кухня" ,
"description" : "..." ,
"yellow_threshold_minutes" : 5 ,
"red_threshold_minutes" : 0 ,
"product_count" : 0 ,
"created_at" : "datetime" ,
"updated_at" : "datetime"
}
}
Errors
Code HTTP Description VALIDATION_ERROR400 Пустое name, name >50 символов DUPLICATE_STATION409 Станция с таким именем (case-insensitive) уже есть во франшизе
PATCH /kitchen-stations/{id}
Обновить станцию.
Параметр Значение Auth Bearer JWT с catalog.edit
Request Body
{
"name" : "string" , // optional
"description" : "string" , // optional
"yellow_threshold_minutes" : 5 , // optional (BR 5.1)
"red_threshold_minutes" : 0 // optional (BR 5.1)
}
При изменении порогов публикуется событие catalog.kds_settings.updated (kind=station_thresholds) — см. Events .
Response 200
Полная запись станции.
Errors
Code HTTP Description STATION_NOT_FOUND404 DUPLICATE_STATION409 VALIDATION_ERROR400 Пороги < 0
DELETE /kitchen-stations/{id}
Soft-delete. Проставляет deleted_at.
Параметр Значение Auth Bearer JWT с catalog.edit
Response 204 — успех
Errors
Code HTTP Description STATION_NOT_FOUND404 STATION_IN_USE422 На станцию ссылается минимум один активный товар. В details — массив { product_id, product_name } первых 10 связанных товаров + total_count
GET /admin/kds/settings
(BR 5.1 — KDS)
Per-франшиза настройки KDS-приложения (звуки, интервалы, авто-логаут). Если запись не существует — auto-create со значениями default и вернуть.
Параметр Значение Auth Bearer JWT с permission catalog.read или kds.settings.edit
Response 200
{
"data" : {
"franchise_id" : "uuid" ,
"new_order_sound" : "bell" ,
"new_order_repeat_seconds" : 30 ,
"overdue_sound" : "alarm" ,
"sound_volume" : 80 ,
"auto_logout_minutes" : 30 ,
"created_at" : "datetime" ,
"updated_at" : "datetime"
}
}
Errors
Code HTTP Description UNAUTHORIZED401 FORBIDDEN403 Нет permission
PATCH /admin/kds/settings
(BR 5.1 — KDS)
Обновление настроек KDS франшизы. Partial update — указываются только изменяемые поля.
Параметр Значение Auth Bearer JWT с kds.settings.edit Content-Type application/json
Request Body
{
"new_order_sound" : "chime" , // optional
"new_order_repeat_seconds" : 45 , // optional, 5–120
"overdue_sound" : "alarm" , // optional
"sound_volume" : 70 , // optional, 0–100
"auto_logout_minutes" : 60 // optional, 5–240
}
Response 200
Обновлённая запись (формат как GET). Публикуется событие catalog.kds_settings.updated с kind=settings — см. Events .
Errors
Code HTTP Description UNAUTHORIZED401 FORBIDDEN403 Нет permission kds.settings.edit VALIDATION_ERROR400 Значение вне допустимого диапазона (детали в details[]) INVALID_SOUND400 Неизвестное имя мелодии (не из встроенного набора bell/chime/buzzer/marimba/digital для new_order и alarm/siren/bell-loud для overdue)
Recipe / техкарты для KDS
Endpoint для получения техкарты блюда (используется в KDS при тапе «?» рядом с позицией) живёт в Warehouse Service : GET /tech-cards?product_id={uuid} (см. Warehouse Service API ). pos-bff проксирует это для KDS как GET /api/v1/pos/products/{id}/recipe.
GET /internal/catalog/full-snapshot
Полный срез каталога франшизы: все товары + группы модификаторов + резолв-мапа категорий. Используется Paykeeper Adapter для ночного cron full re-sync и manual «Пересинхронизировать каталог» (BR 3.4) .
Параметр Значение Auth X-Service-Token
Query Parameters
Параметр Тип Обяз. Описание franchise_iduuid Да Франшиза, чей каталог возвращаем
Response 200
{
"data" : {
"franchise_id" : "uuid" ,
"generated_at" : "ISO-8601 UTC" ,
"products" : [
{
"id" : "uuid" ,
"name" : "string" ,
"category_path" : "string | null" ,
"base_price" : "decimal" ,
"vat_rate" : "string" ,
"unit_of_measure" : "string" ,
"is_marked" : "boolean" ,
"is_open_price" : "boolean" ,
"is_by_weight" : "boolean" ,
"is_alcohol" : "boolean" ,
"modifier_group_ids" : [ "uuid" ],
"deleted_at" : "datetime | null"
}
],
"modifier_groups" : [
{
"id" : "uuid" ,
"group_name" : "string" ,
"binding_type" : "structural | free" ,
"min" : "integer" ,
"max" : "integer" ,
"options" : [
{
"id" : "uuid" ,
"name" : "string" ,
"price" : "decimal | null"
}
]
}
]
}
}
category_path — готовый префикс имени, сформированный из дерева категорий (например "Кофе / Холодное"). Консьюмеру не нужно резолвить иерархию самостоятельно.
Возвращаются все записи включая soft-deleted (поле deleted_at заполнено для фильтрации на стороне консьюмера). Это позволяет adapter’у корректно пометить удалённые товары в своём mapping’е при first-time онбординге, когда событий catalog.*.deleted он пропустил.
Отдельная сущность категорий в PayKeeper ims-api не синхронизируется — категории используются только как часть имени товара через category_path. В snapshot’е отдельного массива категорий нет.
Rate limiting
1 запрос / 10 секунд на один franchise_id. При превышении — 429 RATE_LIMITED с заголовком Retry-After.
Errors
Code HTTP Description FRANCHISE_NOT_FOUND404 franchise_id не существуетVALIDATION_ERROR400 Отсутствует или невалиден franchise_id UNAUTHORIZED401 Нет / невалидный X-Service-Token RATE_LIMITED429 Превышен лимит
GET /internal/catalog/products/{id}/expand
Возвращает раскладку одного товара ERP на виртуальные PK-продукты по правилу развёртывания (спека §Catalog Sync ). Используется Paykeeper Adapter при event-driven delta-sync, чтобы не дёргать full-snapshot ради одного товара (BR 3.4) .
Параметр Значение Auth X-Service-Token
Path Parameters
Параметр Тип Описание iduuid products.id
Response 200
{
"data" : {
"product_id" : "uuid" ,
"franchise_id" : "uuid" ,
"variants" : [
{
"erp_structural_option_id" : "uuid | null" ,
"erp_free_option_id" : "uuid | null" ,
"variant_kind" : "base | structural_variant | free_addon" ,
"name" : "string" ,
"sku" : "string" ,
"price" : "decimal" ,
"tax" : "string"
}
]
}
}
Каждый элемент variants — готовый payload для POST /products в ims-api.
name уже содержит category_path как префикс и развёрнутые имена модификаторов.
sku в формате "{product_id}[:{structural_option_id}][:+{free_option_id}]".
price:
base — базовая цена товара
structural_variant — базовая цена + доплата опции
free_addon — только цена опции (доплата)
Errors
Code HTTP Description PRODUCT_NOT_FOUND404 Товар не существует (или soft-deleted — используй .deleted_at из snapshot’а) UNAUTHORIZED401
Конструктор внешних меню — рекламный монитор, JSON-экспорт. Якорная связь с каталогом обязательна. Stop-list применяется автоматически при рендере. См. спека .
Список внешних меню франшизы.
Параметр Значение Auth Bearer JWT (external_menus.read)
Query Parameters
Параметр Тип Описание channelstring Фильтр: tv_screen / json / опц. statusstring Фильтр: draft / published / all (default: draft,published без archived) store_iduuid Фильтр: показать только меню привязанные к этой ТТ (или включая null-bound) searchstring Поиск по name (debounce клиента) pageint default 1 per_pageint default 20, max 100
Response 200
{
"data" : [
{
"id" : "uuid" ,
"name" : "Бар — основной экран" ,
"channel" : "tv_screen" ,
"store_id" : "uuid | null" ,
"store_name" : "Название ТТ | null" ,
"template" : "grid" ,
"slug" : "bar-mainscreen" ,
"status" : "published" ,
"live_url" : "https://erp-test.nirbi.ru/r/bar-mainscreen" ,
"created_at" : "2026-04-28T10:00:00Z" ,
"updated_at" : "2026-04-28T11:00:00Z"
}
],
"meta" : { "page" : 1 , "per_page" : 20 , "total" : 5 }
}
live_url рассчитывается на сервере: WEBHOOK_BASE_URL + /r/ + slug. Возвращается только для status = published.
Список меню в корзине (архивных) — для вкладки «Корзина». Только за последние 30 дней (более старые удаляются cron’ом физически).
Параметр Значение Auth Bearer JWT (external_menus.edit)
Response 200
Тот же формат что GET /external-menus, но возвращает только status = archived. Дополнительно поле archived_at.
Детали меню с категориями и items (для редактора).
Параметр Значение Auth Bearer JWT (external_menus.read)
Response 200
{
"data" : {
"id" : "uuid" ,
"name" : "Бар — основной экран" ,
"channel" : "tv_screen" ,
"store_id" : "uuid | null" ,
"template" : "grid" ,
"slug" : "bar-mainscreen" ,
"status" : "published" ,
"live_url" : "https://erp-test.nirbi.ru/r/bar-mainscreen" ,
"created_at" : "..." ,
"updated_at" : "..." ,
"categories" : [
{
"id" : "uuid" ,
"name" : "Кофе" ,
"original_category_id" : "uuid | null" ,
"display_order" : 0 ,
"icon_url" : "string | null" ,
"items" : [
{
"id" : "uuid" ,
"product_id" : "uuid" ,
"product_name" : "Капучино" ,
"product_image_url" : "string | null" ,
"catalog_price" : 250.00 ,
"price_list_price" : 270.00 ,
"override_name" : "string | null" ,
"override_description" : "string | null" ,
"override_price" : 290.00 ,
"effective_price" : 290.00 ,
"visible" : true ,
"display_order" : 0 ,
"status" : "ok" ,
"in_stop_list" : false
}
]
}
]
}
}
effective_price рассчитывается на сервере по иерархии override > price_list > catalog. in_stop_list — для индикации владельцу (но при рендере на монитор всё равно скрыто).
Errors
Code HTTP Описание NOT_FOUND404 Меню не найдено или вне scope пользователя
Создать меню (статус draft). Категорий и items нет — добавляются отдельно.
Параметр Значение Auth Bearer JWT (external_menus.edit)
Request Body
{
"name" : "Бар — основной экран" ,
"channel" : "tv_screen" ,
"store_id" : "uuid | null" ,
"template" : "grid | slider | list" ,
"slug" : "string | null"
}
Поле Обяз. Описание nameДа Уникально среди активных в франшизе channelДа tv_screen / jsonstore_idНет NULL — на всю сеть templateУсловно Обязателен для tv_screen, NULL для json slugНет Если NULL — авто-генерируется как menu-{8-char-id}. Regex ^[a-z0-9-]{3,40}$, UNIQUE глобально
Response 201
Идентично GET /external-menus/{id} (categories + items пустые).
Errors
Code HTTP Описание VALIDATION_ERROR400 Невалидные поля, отсутствует template для tv_screen, слишком короткий slug NAME_EXISTS409 Имя уже занято в активных меню SLUG_TAKEN409 slug уже используется STORE_NOT_IN_SCOPE403 ТТ не принадлежит scope пользователя
Обновить заголовок меню (имя, шаблон, slug, ТТ-привязку). channel менять нельзя.
Параметр Значение Auth Bearer JWT (external_menus.edit)
Request Body (partial)
{
"name" : "string" ,
"template" : "grid | slider | list" ,
"slug" : "string" ,
"store_id" : "uuid | null"
}
Response 200
Идентично GET /external-menus/{id}.
Errors
Code HTTP Описание NAME_EXISTS409 SLUG_TAKEN409 CHANNEL_LOCKED422 Попытка сменить channel
Перевести меню в published. Для tv_screen после этого работает live URL.
Параметр Значение Auth Bearer JWT (external_menus.edit)
Response 200
{ "data" : { "id" : "uuid" , "status" : "published" , "live_url" : "..." } }
Errors
Code HTTP Описание BUSINESS_RULE_VIOLATION422 Меню пустое (нет items) — публикация отклонена ALREADY_PUBLISHED422 Уже опубликовано
Вернуть меню в draft. Live URL начинает возвращать 404, WebSocket-клиенты получают menu_unpublished.
Параметр Значение Auth Bearer JWT (external_menus.edit)
Response 200
{ "data" : { "id" : "uuid" , "status" : "draft" } }
Создать копию меню с тем же содержимым (категории + items с overrides). Новое меню в draft, имя {Original} — копия, новый slug menu-{new-8-char-id}.
Параметр Значение Auth Bearer JWT (external_menus.edit)
Response 201
Возвращает новый объект (как POST /external-menus).
Soft delete. Меню переходит в status = archived, заполняется archived_at. Через 30 дней — физическое удаление cron’ом.
Параметр Значение Auth Bearer JWT (external_menus.edit)
Response 204
Live URL сразу 404. WebSocket-клиенты получают menu_archived.
Восстановить из архива. Меню возвращается в status = draft, archived_at = NULL.
Параметр Значение Auth Bearer JWT (external_menus.edit)
Response 200
Идентично GET /external-menus/{id}.
Errors
Code HTTP Описание NOT_FOUND404 Меню не в архиве (или физически удалено cron’ом по истечении 30 дней) NAME_EXISTS409 За время архива создано другое меню с таким же именем — нужно переименовать перед восстановлением SLUG_TAKEN409 Аналогично
Создать категорию внутри меню.
Параметр Значение Auth Bearer JWT (external_menus.edit)
Request Body
{
"name" : "Кофе" ,
"original_category_id" : "uuid | null" ,
"icon_url" : "string | null"
}
Response 201
{
"data" : {
"id" : "uuid" ,
"name" : "Кофе" ,
"original_category_id" : "uuid | null" ,
"display_order" : 0 ,
"icon_url" : null
}
}
display_order назначается автоматически (MAX + 1 в этом меню).
Обновить категорию (имя, иконка).
Параметр Значение Auth Bearer JWT (external_menus.edit)
Request Body (partial)
{ "name" : "string" , "icon_url" : "string | null" }
Удалить категорию вместе со всеми items в ней (CASCADE).
Параметр Значение Auth Bearer JWT (external_menus.edit)
Response 204
Изменить порядок категорий в меню массивом.
Параметр Значение Auth Bearer JWT (external_menus.edit)
Request Body
{
"order" : [
{ "category_id" : "uuid" , "display_order" : 0 },
{ "category_id" : "uuid" , "display_order" : 1 }
]
}
Response 200
{ "data" : { "ok" : true } }
Добавить товар в меню (drag-drop из каталога). Override-поля по умолчанию NULL — наследует значения каталога.
Параметр Значение Auth Bearer JWT (external_menus.edit)
Request Body
{
"product_id" : "uuid" ,
"category_id" : "uuid"
}
Response 201
{
"data" : {
"id" : "uuid" ,
"product_id" : "uuid" ,
"product_name" : "Капучино" ,
"product_image_url" : "string | null" ,
"catalog_price" : 250.00 ,
"price_list_price" : 270.00 ,
"override_name" : null ,
"override_description" : null ,
"override_price" : null ,
"effective_price" : 270.00 ,
"visible" : true ,
"display_order" : 0 ,
"status" : "ok" ,
"in_stop_list" : false
}
}
Errors
Code HTTP Описание PRODUCT_NOT_FOUND404 product_id не существует или soft-deletedPRODUCT_NOT_IN_FRANCHISE422 Товар не принадлежит этой франшизе ITEM_DUPLICATE409 Товар уже есть в этом меню (UNIQUE external_menu_id+product_id) CATEGORY_NOT_FOUND404 category_id не принадлежит этому меню
Обновить override-поля item. null для override_* очищает override (вернёт значение из каталога).
Параметр Значение Auth Bearer JWT (external_menus.edit)
Request Body (partial)
{
"override_name" : "string | null" ,
"override_description" : "string | null" ,
"override_price" : "number | null" ,
"visible" : true ,
"category_id" : "uuid"
}
Изменение category_id перемещает item в другую категорию этого же меню.
Response 200
Идентично POST /external-menus/{id}/items.
Удалить item из меню (физически — это override-запись, не товар каталога).
Параметр Значение Auth Bearer JWT (external_menus.edit)
Response 204
Изменить порядок items внутри категории.
Параметр Значение Auth Bearer JWT (external_menus.edit)
Request Body
{
"category_id" : "uuid" ,
"order" : [
{ "item_id" : "uuid" , "display_order" : 0 },
{ "item_id" : "uuid" , "display_order" : 1 }
]
}
Response 200
{ "data": { "ok": true } }
Снять статус orphan с item (если в каталоге товар восстановили из soft-delete).
Параметр Значение Auth Bearer JWT (external_menus.edit)
Response 200
{ "data" : { "id" : "uuid" , "status" : "ok" } }
Errors
Code HTTP Описание BUSINESS_RULE_VIOLATION422 Товар каталога всё ещё удалён — нельзя восстановить item пока товар не восстановлен
Универсальный JSON-экспорт меню. Stop-list применён, orphan-items скрыты, override применены.
Параметр Значение Auth Bearer JWT (external_menus.read)
Response 200
{
"menu_id" : "uuid" ,
"name" : "Бар — основной экран" ,
"channel" : "json" ,
"store_id" : "uuid | null" ,
"generated_at" : "2026-04-28T15:00:00Z" ,
"categories" : [
{
"id" : "uuid" ,
"name" : "Кофе" ,
"items" : [
{
"product_id" : "uuid" ,
"name" : "Капучино" ,
"description" : "string | null" ,
"price" : 290.00 ,
"image_url" : "string | null"
}
]
}
]
}
Errors
Code HTTP Описание MENU_NOT_PUBLISHED422 Меню в draft — экспорт недоступен
Скачать offline ZIP-архив для канала tv_screen. Содержит index.html + style.css + assets/ со снапшотом меню на момент скачивания.
Параметр Значение Auth Bearer JWT (external_menus.edit)
Response 200
Content-Type: application/zip. Файл ERP-POS-menu-{slug}-{date}.zip.
Errors
Code HTTP Описание INVALID_CHANNEL422 Меню не tv_screen — ZIP не предусмотрен
GET /r/{slug}
Public render endpoint для рекламного монитора. Открывается в Chromium kiosk-mode на железе монитора. Не требует авторизации — защита через знание slug.
Параметр Значение Auth Public (slug — secret enough)
Response 200
HTML-страница с шаблоном (grid / slider / list), embedded data, JS для WebSocket подключения.
Response 404
Меню не найдено
Меню в status != published
Меню в архиве
Query Parameters
Параметр Тип Описание previewbool 1 — preview-режим, рендерит draft (требует preview-токен в cookie или header)
GET /r/{slug}/data
JSON для AJAX-обновления (используется когда WebSocket недоступен — fallback к polling).
Параметр Значение Auth Public
Response 200
{
"menu_id" : "uuid" ,
"version_hash" : "string — SHA-256 от состояния меню для оптимизации" ,
"categories" : [
{
"id" : "uuid" ,
"name" : "Кофе" ,
"icon_url" : "string | null" ,
"items" : [
{
"id" : "uuid" ,
"product_id" : "uuid" ,
"name" : "Капучино" ,
"description" : "string | null" ,
"price" : 290.00 ,
"image_url" : "string | null" ,
"display_order" : 0
}
]
}
]
}
version_hash — клиент сравнивает с прошлой версией; если не изменилось — не перерисовывает DOM.
WebSocket /r/{slug}/stream
WebSocket для live-обновлений. Сервер шлёт menu_updated при изменении меню. Клиент при получении делает GET /r/{slug}/data для свежих данных.
Параметр Значение Auth Public Protocol WebSocket
Server messages
{ "type" : "menu_updated" , "version_hash" : "string" }
{ "type" : "menu_unpublished" }
{ "type" : "menu_archived" }
{ "type" : "ping" }
Client messages
{ "type" : "pong" }
Расширение формы товара (BR 2.5)
Добавлены поля в existing endpoint-ах POST /products, PATCH /products/{id}, GET /products/{id}:
requires_kitchen (boolean, default false)
kitchen_station_id (uuid, nullable)
Валидация при сохранении (POST/PATCH):
Если requires_kitchen = true и kitchen_station_id IS NULL → 422 MISSING_KITCHEN_STATION
Если requires_kitchen = false и kitchen_station_id передан → сервис автоматически сбрасывает kitchen_station_id = NULL (не отклоняет запрос)
Если kitchen_station_id указывает на несуществующую / soft-deleted станцию → 422 STATION_NOT_FOUND
Общий формат ошибок
{
"error" : {
"code" : "string — машиночитаемый код (UPPER_SNAKE_CASE)" ,
"message" : "string — человекочитаемое описание" ,
"details" : [
{
"field" : "string — путь к полю" ,
"message" : "string — описание ошибки"
}
]
}
}