# Полная документация Backend API > **Последнее обновление:** Февраль 2026 > **Фронтенд:** Angular 21 · Два бренда (Dexar + Novo) > **Охватывает:** Каталог, Корзина, Оплата, Отзывы, Регионы, Авторизация, i18n, BackOffice --- ## Базовые URL | Бренд | Dev | Production | |--------|----------------------------------|----------------------------------| | Dexar | `https://api.dexarmarket.ru:445` | `https://api.dexarmarket.ru:445` | | Novo | `https://api.novo.market:444` | `https://api.novo.market:444` | --- ## Глобальные HTTP-заголовки Фронтенд **автоматически добавляет** два кастомных заголовка к **каждому API-запросу** через interceptor. Бэкенд должен читать эти заголовки и использовать для фильтрации/перевода ответов. | Заголовок | Пример значения | Описание | |---------------|-----------------|-------------------------------------------------------------------| | `X-Region` | `moscow` | ID региона, выбранного пользователем. **Отсутствует** = все регионы. | | `X-Language` | `ru` | Активный язык интерфейса: `ru`, `en` или `hy`. | ### Поведение бэкенда - **`X-Region`**: Если присутствует — фильтровать товары/категории только по этому региону. Если отсутствует — возвращать всё (глобальный каталог). - **`X-Language`**: Если присутствует — возвращать переведённые `name`, `description` и т.д., если переводы существуют. Если отсутствует или `ru` — возвращать на русском (по умолчанию). ### Требования CORS для этих заголовков ``` Access-Control-Allow-Headers: Content-Type, X-Region, X-Language ``` --- ## 1. Проверка состояния ### `GET /ping` Простая проверка работоспособности. **Ответ `200`:** ```json { "message": "pong" } ``` --- ## 2. Каталог — Категории ### `GET /category` Возвращает все категории верхнего уровня. Учитывает заголовки `X-Region` и `X-Language`. **Ответ `200`:** ```json [ { "categoryID": 1, "name": "Электроника", "parentID": 0, "icon": "https://...", "wideBanner": "https://...", "itemCount": 42, "priority": 10, "id": "cat_abc123", "visible": true, "img": "https://...", "projectId": "proj_xyz", "subcategories": [ { "id": "sub_001", "name": "Смартфоны", "visible": true, "priority": 5, "img": "https://...", "categoryId": "cat_abc123", "parentId": "cat_abc123", "itemCount": 20, "hasItems": true, "subcategories": [] } ] } ] ``` **Объект Category:** | Поле | Тип | Обязат. | Описание | |------------------|---------------|---------|-----------------------------------------------------| | `categoryID` | number | да | Числовой ID (legacy) | | `name` | string | да | Название категории (переведённое если `X-Language`) | | `parentID` | number | да | ID родительской категории (`0` = верхний уровень) | | `icon` | string | нет | URL иконки категории | | `wideBanner` | string | нет | URL широкого баннера | | `itemCount` | number | нет | Количество товаров в категории | | `priority` | number | нет | Приоритет сортировки (больше = выше) | | `id` | string | нет | Строковый ID из BackOffice | | `visible` | boolean | нет | Видима ли категория (по умолч. `true`) | | `img` | string | нет | URL изображения из BackOffice (маппится на `icon`) | | `projectId` | string | нет | Ссылка на проект в BackOffice | | `subcategories` | Subcategory[] | нет | Вложенные подкатегории | **Объект Subcategory:** | Поле | Тип | Обязат. | Описание | |------------------|---------------|---------|----------------------------------| | `id` | string | да | ID подкатегории | | `name` | string | да | Отображаемое название | | `visible` | boolean | нет | Видима ли | | `priority` | number | нет | Приоритет сортировки | | `img` | string | нет | URL изображения | | `categoryId` | string | да | ID родительской категории | | `parentId` | string | да | ID прямого родителя | | `itemCount` | number | нет | Количество товаров | | `hasItems` | boolean | нет | Есть ли товары | | `subcategories` | Subcategory[] | нет | Вложенные дочерние подкатегории | --- ### `GET /category/:categoryID` Возвращает товары определённой категории. Учитывает заголовки `X-Region` и `X-Language`. **Query-параметры:** | Параметр | Тип | По умолч. | Описание | |----------|--------|-----------|-----------------------| | `count` | number | `50` | Товаров на страницу | | `skip` | number | `0` | Смещение для пагинации | **Ответ `200`:** Массив объектов [Item](#объект-item). --- ## 3. Товары ### `GET /item/:itemID` Возвращает один товар. Учитывает заголовки `X-Region` и `X-Language`. **Ответ `200`:** Один объект [Item](#объект-item). --- ### `GET /searchitems` Полнотекстовый поиск по товарам. Учитывает заголовки `X-Region` и `X-Language`. **Query-параметры:** | Параметр | Тип | По умолч. | Описание | |----------|--------|-----------|-------------------------------| | `search` | string | — | Поисковый запрос (обязателен) | | `count` | number | `50` | Товаров на страницу | | `skip` | number | `0` | Смещение для пагинации | **Ответ `200`:** ```json { "items": [ /* объекты Item */ ], "total": 128, "count": 50, "skip": 0 } ``` --- ### `GET /randomitems` Возвращает случайные товары для карусели/рекомендаций. Учитывает заголовки `X-Region` и `X-Language`. **Query-параметры:** | Параметр | Тип | По умолч. | Описание | |------------|--------|-----------|--------------------------------------| | `count` | number | `5` | Количество товаров | | `category` | number | — | Ограничить данной категорией (опц.) | **Ответ `200`:** Массив объектов [Item](#объект-item). --- ### Объект Item Бэкенд может возвращать товары в **любом** из двух форматов — legacy или BackOffice. Фронтенд нормализует оба варианта. ```json { "categoryID": 1, "itemID": 123, "name": "iPhone 15 Pro", "photos": [{ "url": "https://..." }], "description": "Описание товара", "currency": "RUB", "price": 89990, "discount": 10, "remainings": "high", "rating": 4.5, "callbacks": [ { "rating": 5, "content": "Отличный товар!", "userID": "user_123", "timestamp": "2026-02-01T12:00:00Z" } ], "questions": [ { "question": "Есть ли гарантия?", "answer": "Да, 12 месяцев", "upvotes": 5, "downvotes": 0 } ], "id": "item_abc123", "visible": true, "priority": 10, "imgs": ["https://img1.jpg", "https://img2.jpg"], "tags": ["new", "popular"], "badges": ["bestseller", "sale"], "simpleDescription": "Краткое описание", "descriptionFields": [ { "key": "Процессор", "value": "A17 Pro" }, { "key": "Память", "value": "256 GB" } ], "subcategoryId": "sub_001", "translations": { "en": { "name": "iPhone 15 Pro", "simpleDescription": "Short description", "description": [ { "key": "Processor", "value": "A17 Pro" } ] }, "hy": { "name": "iPhone 15 Pro", "simpleDescription": "Կարcheck check" } }, "comments": [ { "id": "cmt_001", "text": "Отличный товар!", "author": "user_123", "stars": 5, "createdAt": "2026-02-01T12:00:00Z" } ], "quantity": 50 } ``` **Все поля Item:** | Поле | Тип | Обязат. | Описание | |---------------------|-------------------|---------|-----------------------------------------------------------| | `categoryID` | number | да | Категория, к которой принадлежит товар | | `itemID` | number | да | Числовой ID товара (legacy) | | `name` | string | да | Название товара | | `photos` | Photo[] | нет | Массив фотографий `[{ url }]` (legacy) | | `description` | string | да | Текстовое описание | | `currency` | string | да | Код валюты (по умолч. `RUB`) | | `price` | number | да | Цена | | `discount` | number | да | Процент скидки (`0`–`100`) | | `remainings` | string | нет | Уровень остатка: `high`, `medium`, `low`, `out` | | `rating` | number | да | Средний рейтинг (`0`–`5`) | | `callbacks` | Review[] | нет | Отзывы (legacy формат) | | `questions` | Question[] | нет | Вопросы и ответы | | `id` | string | нет | Строковый ID из BackOffice | | `visible` | boolean | нет | Виден ли товар (по умолч. `true`) | | `priority` | number | нет | Приоритет сортировки (больше = выше) | | `imgs` | string[] | нет | URL картинок из BackOffice (маппится на `photos`) | | `tags` | string[] | нет | Теги для фильтрации | | `badges` | string[] | нет | Бейджи (`bestseller`, `sale` и т.д.) | | `simpleDescription` | string | нет | Краткое текстовое описание | | `descriptionFields` | DescriptionField[]| нет | Структурированное описание `[{ key, value }]` | | `subcategoryId` | string | нет | Ссылка на подкатегорию из BackOffice | | `translations` | Record | нет | Переводы по ключу языка (см. ниже) | | `comments` | Comment[] | нет | Комментарии в формате BackOffice | | `quantity` | number | нет | Числовое кол-во на складе (маппится на `remainings`) | **Вложенные типы:** | Тип | Поля | |--------------------|-----------------------------------------------------------------| | `Photo` | `url: string`, `photo?: string`, `video?: string`, `type?: string` | | `DescriptionField` | `key: string`, `value: string` | | `Comment` | `id?: string`, `text: string`, `author?: string`, `stars?: number`, `createdAt?: string` | | `Review` | `rating?: number`, `content?: string`, `userID?: string`, `answer?: string`, `timestamp?: string` | | `Question` | `question: string`, `answer: string`, `upvotes: number`, `downvotes: number` | | `ItemTranslation` | `name?: string`, `simpleDescription?: string`, `description?: DescriptionField[]` | --- ## 4. Корзина ### `POST /cart` — Добавить товар в корзину **Тело запроса:** ```json { "itemID": 123, "quantity": 1 } ``` **Ответ `200`:** ```json { "message": "Added to cart" } ``` --- ### `PATCH /cart` — Обновить количество товара **Тело запроса:** ```json { "itemID": 123, "quantity": 3 } ``` **Ответ `200`:** ```json { "message": "Updated" } ``` --- ### `DELETE /cart` — Удалить товары из корзины **Тело запроса:** Массив ID товаров ```json [123, 456] ``` **Ответ `200`:** ```json { "message": "Removed" } ``` --- ### `GET /cart` — Получить содержимое корзины **Ответ `200`:** Массив объектов [Item](#объект-item) (каждый с полем `quantity`). --- ## 5. Оплата (СБП / QR) ### `POST /cart` — Создать платёж (СБП QR) > Примечание: Тот же эндпоинт что и добавление в корзину, но с другой схемой тела запроса. **Тело запроса:** ```json { "amount": 89990, "currency": "RUB", "siteuserID": "tg_123456789", "siteorderID": "order_abc123", "redirectUrl": "", "telegramUsername": "john_doe", "items": [ { "itemID": 123, "price": 89990, "name": "iPhone 15 Pro" } ] } ``` **Ответ `200`:** ```json { "qrId": "qr_abc123", "qrStatus": "CREATED", "qrExpirationDate": "2026-02-28T13:00:00Z", "payload": "https://qr.nspk.ru/...", "qrUrl": "https://qr.nspk.ru/..." } ``` --- ### `GET /qr/payment/:qrId` — Проверить статус оплаты **Ответ `200`:** ```json { "additionalInfo": "", "paymentPurpose": "Order #order_abc123", "amount": 89990, "code": "SUCCESS", "createDate": "2026-02-28T12:00:00Z", "currency": "RUB", "order": "order_abc123", "paymentStatus": "COMPLETED", "qrId": "qr_abc123", "transactionDate": "2026-02-28T12:01:00Z", "transactionId": 999, "qrExpirationDate": "2026-02-28T13:00:00Z", "phoneNumber": "+7XXXXXXXXXX" } ``` | Значение `paymentStatus` | Значение | |--------------------------|------------------------------| | `CREATED` | QR создан, не оплачен | | `WAITING` | Оплата в процессе | | `COMPLETED` | Оплата успешна | | `EXPIRED` | QR-код истёк | | `CANCELLED` | Оплата отменена | --- ### `POST /purchase-email` — Отправить email после оплаты **Тело запроса:** ```json { "email": "user@example.com", "telegramUserId": "123456789", "items": [ { "itemID": 123, "name": "iPhone 15 Pro", "price": 89990, "currency": "RUB" } ] } ``` **Ответ `200`:** ```json { "message": "Email sent" } ``` --- ## 6. Отзывы / Комментарии ### `POST /comment` — Оставить отзыв **Тело запроса:** ```json { "itemID": 123, "rating": 5, "comment": "Отличный товар!", "username": "john_doe", "userId": 123456789, "timestamp": "2026-02-28T12:00:00Z" } ``` **Ответ `200`:** ```json { "message": "Review submitted" } ``` --- ## 7. Регионы ### `GET /regions` — Список доступных регионов Возвращает регионы, в которых работает маркетплейс. **Ответ `200`:** ```json [ { "id": "moscow", "city": "Москва", "country": "Россия", "countryCode": "RU", "timezone": "Europe/Moscow" }, { "id": "spb", "city": "Санкт-Петербург", "country": "Россия", "countryCode": "RU", "timezone": "Europe/Moscow" }, { "id": "yerevan", "city": "Ереван", "country": "Армения", "countryCode": "AM", "timezone": "Asia/Yerevan" } ] ``` **Объект Region:** | Поле | Тип | Обязат. | Описание | |---------------|--------|---------|----------------------------------| | `id` | string | да | Уникальный идентификатор региона | | `city` | string | да | Название города | | `country` | string | да | Название страны | | `countryCode` | string | да | Код страны ISO 3166-1 alpha-2 | | `timezone` | string | нет | Часовой пояс IANA | > **Фоллбэк:** Если эндпоинт недоступен, фронтенд использует 6 захардкоженных значений: Москва, СПб, Ереван, Минск, Алматы, Тбилиси. --- ## 8. Авторизация (вход через Telegram) Авторизация **через Telegram** с **cookie-сессиями** (HttpOnly, Secure, SameSite=None). Все auth-эндпоинты должны поддерживать CORS с `credentials: true`. ### Процесс авторизации ``` 1. Пользователь нажимает «Оформить заказ» → не авторизован → показывается диалог входа 2. Нажимает «Войти через Telegram» → открывается https://t.me/{bot}?start=auth_{callback} 3. Пользователь запускает бота в Telegram 4. Бот отправляет данные пользователя → бэкенд /auth/telegram/callback 5. Бэкенд создаёт сессию → устанавливает Set-Cookie 6. Фронтенд опрашивает GET /auth/session каждые 3 секунды 7. Сессия обнаружена → диалог закрывается → оформление заказа продолжается ``` --- ### `GET /auth/session` — Проверить текущую сессию **Запрос:** Только cookie (сессионная cookie, установленная бэкендом). **Ответ `200`** (авторизован): ```json { "sessionId": "sess_abc123", "telegramUserId": 123456789, "username": "john_doe", "displayName": "John Doe", "active": true, "expiresAt": "2026-03-01T12:00:00Z" } ``` **Ответ `200`** (сессия истекла): ```json { "sessionId": "sess_abc123", "telegramUserId": 123456789, "username": "john_doe", "displayName": "John Doe", "active": false, "expiresAt": "2026-02-27T12:00:00Z" } ``` **Ответ `401`** (нет сессии): ```json { "error": "No active session" } ``` **Объект AuthSession:** | Поле | Тип | Обязат. | Описание | |------------------|---------|---------|-------------------------------------------| | `sessionId` | string | да | Уникальный ID сессии | | `telegramUserId` | number | да | ID пользователя в Telegram | | `username` | string? | нет | @username в Telegram (может быть null) | | `displayName` | string | да | Отображаемое имя (имя + фамилия) | | `active` | boolean | да | Действительна ли сессия | | `expiresAt` | string | да | Дата истечения в формате ISO 8601 | --- ### `GET /auth/telegram/callback` — Callback авторизации Telegram-бота Вызывается Telegram-ботом после авторизации пользователя. **Тело запроса (от бота):** ```json { "id": 123456789, "first_name": "John", "last_name": "Doe", "username": "john_doe", "photo_url": "https://t.me/i/userpic/...", "auth_date": 1709100000, "hash": "abc123def456..." } ``` **Ответ:** Должен установить cookie сессии и вернуть: ```json { "sessionId": "sess_abc123", "message": "Authenticated successfully" } ``` **Требования к cookie:** | Атрибут | Значение | Примечание | |------------|----------------|-----------------------------------------------------| | `HttpOnly` | `true` | Недоступна из JavaScript | | `Secure` | `true` | Только HTTPS | | `SameSite` | `None` | Обязательно для cross-origin (API ≠ фронтенд) | | `Path` | `/` | | | `Max-Age` | `86400` (24ч) | Или по необходимости | --- ### `POST /auth/logout` — Завершить сессию **Запрос:** Только cookie, пустое тело `{}` **Ответ `200`:** ```json { "message": "Logged out" } ``` Должен очистить/инвалидировать cookie сессии. --- ### Обновление сессии Фронтенд повторно проверяет сессию за **60 секунд до `expiresAt`**. Если бэкенд поддерживает скользящий срок действия (sliding expiration), можно обновлять `Max-Age` cookie при каждом вызове `GET /auth/session`. --- ## 9. i18n / Переводы Фронтенд поддерживает 3 языка: **Русский (ru)**, **Английский (en)**, **Армянский (hy)**. Активный язык отправляется через HTTP-заголовок `X-Language` с каждым запросом. ### Что бэкенд должен делать с `X-Language` 1. **Категории и товары**: Если для запрошенного языка есть поле `translations`, вернуть переведённые `name`, `description` и т.д. ИЛИ бэкенд может применять переводы на стороне сервера и возвращать уже переведённые поля. 2. **Поле `translations`** на товарах (опциональный подход): ```json { "translations": { "en": { "name": "iPhone 15 Pro", "simpleDescription": "Short desc in English", "description": [{ "key": "Processor", "value": "A17 Pro" }] }, "hy": { "name": "iPhone 15 Pro", "simpleDescription": "Կarcheck check" } } } ``` 3. **Рекомендуемый подход**: Читать заголовок `X-Language` и возвращать `name`/`description` на этом языке напрямую. Если перевода нет — возвращать русский вариант по умолчанию. --- ## 10. Настройка CORS Для работы auth-cookie и кастомных заголовков конфигурация CORS бэкенда должна включать: ``` Access-Control-Allow-Origin: https://dexarmarket.ru (НЕ wildcard *) Access-Control-Allow-Credentials: true Access-Control-Allow-Headers: Content-Type, X-Region, X-Language Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONS ``` > **Важно:** `Access-Control-Allow-Origin` не может быть `*` при `Allow-Credentials: true`. Должен быть точный origin фронтенда. **Разрешённые origins:** - `https://dexarmarket.ru` - `https://novo.market` - `http://localhost:4200` (dev) - `http://localhost:4201` (dev, Novo) --- ## 11. Настройка Telegram-бота Каждому бренду нужен свой бот: - **Dexar:** `@dexarmarket_bot` - **Novo:** `@novomarket_bot` Бот должен: 1. Слушать команду `/start auth_{callbackUrl}` 2. Извлечь callback URL 3. Отправить данные пользователя (`id`, `first_name`, `username` и т.д.) на этот callback URL 4. Callback URL: `{apiUrl}/auth/telegram/callback` --- ## Полный справочник эндпоинтов ### Новые эндпоинты | Метод | Путь | Описание | Авторизация | |--------|---------------------------|---------------------------------|-------------| | `GET` | `/regions` | Список доступных регионов | Нет | | `GET` | `/auth/session` | Проверка текущей сессии | Cookie | | `GET` | `/auth/telegram/callback` | Callback авторизации через бота | Нет (бот) | | `POST` | `/auth/logout` | Завершение сессии | Cookie | ### Существующие эндпоинты | Метод | Путь | Описание | Авт. | Заголовки | |----------|-----------------------|---------------------------|------|--------------------| | `GET` | `/ping` | Проверка состояния | Нет | — | | `GET` | `/category` | Список категорий | Нет | X-Region, X-Language | | `GET` | `/category/:id` | Товары категории | Нет | X-Region, X-Language | | `GET` | `/item/:id` | Один товар | Нет | X-Region, X-Language | | `GET` | `/searchitems` | Поиск товаров | Нет | X-Region, X-Language | | `GET` | `/randomitems` | Случайные товары | Нет | X-Region, X-Language | | `POST` | `/cart` | Добавить в корзину / Оплата | Нет* | — | | `PATCH` | `/cart` | Обновить кол-во | Нет* | — | | `DELETE` | `/cart` | Удалить из корзины | Нет* | — | | `GET` | `/cart` | Содержимое корзины | Нет* | — | | `POST` | `/comment` | Оставить отзыв | Нет | — | | `GET` | `/qr/payment/:qrId` | Статус оплаты | Нет | — | | `POST` | `/purchase-email` | Отправить email после оплаты | Нет | — | > \* Эндпоинты корзины/оплаты могут использовать cookie сессии (если есть) для привязки к заказу, но не требуют авторизации строго. Фронтенд проверяет авторизацию перед оформлением заказа.