# Complete Backend API Documentation > **Last updated:** February 2026 > **Frontend:** Angular 21 · Dual-brand (Dexar + Novo) > **Covers:** Catalog, Cart, Payments, Reviews, Regions, Auth, i18n, BackOffice --- ## Base URLs | Brand | Dev | Production | |--------|----------------------------------|----------------------------------| | Dexar | `https://api.dexarmarket.ru:445` | `https://api.dexarmarket.ru:445` | | Novo | `https://api.novo.market:444` | `https://api.novo.market:444` | --- ## Global HTTP Headers The frontend **automatically attaches** two custom headers to **every API request** via an interceptor. The backend should read these headers and use them to filter/translate responses accordingly. | Header | Example Value | Description | |---------------|---------------|------------------------------------------------------------| | `X-Region` | `moscow` | Region ID selected by the user. **Absent** = global (all). | | `X-Language` | `ru` | Active UI language: `ru`, `en`, or `hy`. | ### Backend behavior - **`X-Region`**: If present, filter items/categories to only those available in that region. If absent, return everything (global catalog). - **`X-Language`**: If present, return translated `name`, `description`, etc. for categories/items when translations exist. If absent or `ru`, use russians defaults. ### CORS requirements for these headers ``` Access-Control-Allow-Headers: Content-Type, X-Region, X-Language ``` --- ## 1. Health Check ### `GET /ping` Simple health check. **Response `200`:** ```json { "message": "pong" } ``` --- ## 2. Catalog — Categories ### `GET /category` Returns all top-level categories. Respects `X-Region` and `X-Language` headers. **Response `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 object:** | Field | Type | Required | Description | |------------------|---------------|----------|----------------------------------------------------| | `categoryID` | number | yes | Legacy numeric ID | | `name` | string | yes | Category display name (translated if `X-Language`) | | `parentID` | number | yes | Parent category ID (`0` = top-level) | | `icon` | string | no | Category icon URL | | `wideBanner` | string | no | Wide banner image URL | | `itemCount` | number | no | Number of items in category | | `priority` | number | no | Sort priority (higher = first) | | `id` | string | no | BackOffice string ID | | `visible` | boolean | no | Whether category is shown (`true` default) | | `img` | string | no | BackOffice image URL (maps to `icon`) | | `projectId` | string | no | BackOffice project reference | | `subcategories` | Subcategory[] | no | Nested subcategories | **Subcategory object:** | Field | Type | Required | Description | |------------------|---------------|----------|------------------------------------| | `id` | string | yes | Subcategory ID | | `name` | string | yes | Display name | | `visible` | boolean | no | Whether visible | | `priority` | number | no | Sort priority | | `img` | string | no | Image URL | | `categoryId` | string | yes | Parent category ID | | `parentId` | string | yes | Direct parent ID | | `itemCount` | number | no | Number of items | | `hasItems` | boolean | no | Whether has any items | | `subcategories` | Subcategory[] | no | Nested children | --- ### `GET /category/:categoryID` Returns items in a specific category. Respects `X-Region` and `X-Language` headers. **Query params:** | Param | Type | Default | Description | |----------|--------|---------|--------------------| | `count` | number | `50` | Items per page | | `skip` | number | `0` | Offset for paging | **Response `200`:** Array of [Item](#item-object) objects. --- ## 3. Items ### `GET /item/:itemID` Returns a single item. Respects `X-Region` and `X-Language` headers. **Response `200`:** A single [Item](#item-object) object. --- ### `GET /searchitems` Full-text search across items. Respects `X-Region` and `X-Language` headers. **Query params:** | Param | Type | Default | Description | |----------|--------|---------|----------------------| | `search` | string | — | Search query (required) | | `count` | number | `50` | Items per page | | `skip` | number | `0` | Offset for paging | **Response `200`:** ```json { "items": [ /* Item objects */ ], "total": 128, "count": 50, "skip": 0 } ``` --- ### `GET /randomitems` Returns random items for carousel/recommendations. Respects `X-Region` and `X-Language` headers. **Query params:** | Param | Type | Default | Description | |------------|--------|---------|------------------------------------| | `count` | number | `5` | Number of items to return | | `category` | number | — | Optional: limit to this category | **Response `200`:** Array of [Item](#item-object) objects. --- ### Item Object The backend can return items in **either** legacy format or BackOffice format. The frontend normalizes both. ```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 check" } }, "comments": [ { "id": "cmt_001", "text": "Отличный товар!", "author": "user_123", "stars": 5, "createdAt": "2026-02-01T12:00:00Z" } ], "quantity": 50 } ``` **Full Item fields:** | Field | Type | Required | Description | |---------------------|-------------------|----------|------------------------------------------------------------| | `categoryID` | number | yes | Category this item belongs to | | `itemID` | number | yes | Legacy numeric item ID | | `name` | string | yes | Item display name | | `photos` | Photo[] | no | Legacy photo array `[{ url }]` | | `description` | string | yes | Text description | | `currency` | string | yes | Currency code (default: `RUB`) | | `price` | number | yes | Price in the currency's smallest display unit | | `discount` | number | yes | Discount percentage (`0`–`100`) | | `remainings` | string | no | Stock level: `high`, `medium`, `low`, `out` | | `rating` | number | yes | Average rating (`0`–`5`) | | `callbacks` | Review[] | no | Legacy reviews (alias for reviews) | | `questions` | Question[] | no | Q&A entries | | `id` | string | no | BackOffice string ID | | `visible` | boolean | no | Whether item is visible (`true` default) | | `priority` | number | no | Sort priority (higher = first) | | `imgs` | string[] | no | BackOffice image URLs (maps to `photos`) | | `tags` | string[] | no | Item tags for filtering | | `badges` | string[] | no | Display badges (`bestseller`, `sale`, etc.) | | `simpleDescription` | string | no | Short plain-text description | | `descriptionFields` | DescriptionField[]| no | Structured `[{ key, value }]` descriptions | | `subcategoryId` | string | no | BackOffice subcategory reference | | `translations` | Record | no | Translations keyed by lang code (see below) | | `comments` | Comment[] | no | BackOffice comments format | | `quantity` | number | no | Numeric stock count (maps to `remainings` on frontend) | **Nested types:** | Type | Fields | |--------------------|-----------------------------------------------------------------| | `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. Cart ### `POST /cart` — Add item to cart **Request body:** ```json { "itemID": 123, "quantity": 1 } ``` **Response `200`:** ```json { "message": "Added to cart" } ``` --- ### `PATCH /cart` — Update item quantity **Request body:** ```json { "itemID": 123, "quantity": 3 } ``` **Response `200`:** ```json { "message": "Updated" } ``` --- ### `DELETE /cart` — Remove items from cart **Request body:** Array of item IDs ```json [123, 456] ``` **Response `200`:** ```json { "message": "Removed" } ``` --- ### `GET /cart` — Get cart contents **Response `200`:** Array of [Item](#item-object) objects (each with `quantity` field). --- ## 5. Payments (SBP / QR) ### `POST /cart` — Create payment (SBP QR) > Note: Same endpoint as add-to-cart but with different body schema. **Request body:** ```json { "amount": 89990, "currency": "RUB", "siteuserID": "tg_123456789", "siteorderID": "order_abc123", "redirectUrl": "", "telegramUsername": "john_doe", "items": [ { "itemID": 123, "price": 89990, "name": "iPhone 15 Pro" } ] } ``` **Response `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` — Check payment status **Response `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` values | Meaning | |------------------------|---------------------------| | `CREATED` | QR generated, not paid | | `WAITING` | Payment in progress | | `COMPLETED` | Payment successful | | `EXPIRED` | QR code expired | | `CANCELLED` | Payment cancelled | --- ### `POST /purchase-email` — Submit email after payment **Request body:** ```json { "email": "user@example.com", "telegramUserId": "123456789", "items": [ { "itemID": 123, "name": "iPhone 15 Pro", "price": 89990, "currency": "RUB" } ] } ``` **Response `200`:** ```json { "message": "Email sent" } ``` --- ## 6. Reviews / Comments ### `POST /comment` — Submit a review **Request body:** ```json { "itemID": 123, "rating": 5, "comment": "Great product!", "username": "john_doe", "userId": 123456789, "timestamp": "2026-02-28T12:00:00Z" } ``` **Response `200`:** ```json { "message": "Review submitted" } ``` --- ## 7. Regions ### `GET /regions` — List available regions Returns regions where the marketplace operates. **Response `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 object:** | Field | Type | Required | Description | |---------------|--------|----------|--------------------------| | `id` | string | yes | Unique region identifier | | `city` | string | yes | City name (display) | | `country` | string | yes | Country name | | `countryCode` | string | yes | ISO 3166-1 alpha-2 | | `timezone` | string | no | IANA timezone | > **Fallback:** If this endpoint is down, the frontend uses 6 hardcoded defaults: Moscow, SPB, Yerevan, Minsk, Almaty, Tbilisi. --- ## 8. Authentication (Telegram Login) Authentication is **Telegram-based** with **cookie sessions** (HttpOnly, Secure, SameSite=None). All auth endpoints must include `withCredentials: true` CORS support. ### Auth flow ``` 1. User clicks "Checkout" → not authenticated → login dialog shown 2. User clicks "Log in with Telegram" → opens https://t.me/{bot}?start=auth_{callback} 3. User starts the bot in Telegram 4. Bot sends user data → backend /auth/telegram/callback 5. Backend creates session → sets Set-Cookie 6. Frontend polls GET /auth/session every 3s 7. Session detected → dialog closes → checkout proceeds ``` --- ### `GET /auth/session` — Check current session **Request:** Cookies only (session cookie set by backend). **Response `200`** (authenticated): ```json { "sessionId": "sess_abc123", "telegramUserId": 123456789, "username": "john_doe", "displayName": "John Doe", "active": true, "expiresAt": "2026-03-01T12:00:00Z" } ``` **Response `200`** (expired): ```json { "sessionId": "sess_abc123", "telegramUserId": 123456789, "username": "john_doe", "displayName": "John Doe", "active": false, "expiresAt": "2026-02-27T12:00:00Z" } ``` **Response `401`** (no session): ```json { "error": "No active session" } ``` **AuthSession object:** | Field | Type | Required | Description | |------------------|---------|----------|--------------------------------------------| | `sessionId` | string | yes | Unique session ID | | `telegramUserId` | number | yes | Telegram user ID | | `username` | string? | no | Telegram @username (can be null) | | `displayName` | string | yes | User display name (first + last) | | `active` | boolean | yes | Whether session is valid | | `expiresAt` | string | yes | ISO 8601 expiration datetime | --- ### `GET /auth/telegram/callback` — Telegram bot auth callback Called by the Telegram bot after user authenticates. **Request body (from bot):** ```json { "id": 123456789, "first_name": "John", "last_name": "Doe", "username": "john_doe", "photo_url": "https://t.me/i/userpic/...", "auth_date": 1709100000, "hash": "abc123def456..." } ``` **Response:** Must set a session cookie and return: ```json { "sessionId": "sess_abc123", "message": "Authenticated successfully" } ``` **Cookie requirements:** | Attribute | Value | Notes | |------------|----------------|--------------------------------------------| | `HttpOnly` | `true` | Not accessible via JS | | `Secure` | `true` | HTTPS only | | `SameSite` | `None` | Required for cross-origin (API ≠ frontend) | | `Path` | `/` | | | `Max-Age` | `86400` (24h) | Or as needed | --- ### `POST /auth/logout` — End session **Request:** Cookies only, empty body `{}` **Response `200`:** ```json { "message": "Logged out" } ``` Must clear/invalidate the session cookie. --- ### Session refresh The frontend re-checks the session **60 seconds before `expiresAt`**. If the backend supports sliding expiration, it can reset the cookie's `Max-Age` on each `GET /auth/session`. --- ## 9. i18n / Translations The frontend supports 3 languages: **Russian (ru)**, **English (en)**, **Armenian (hy)**. The active language is sent via the `X-Language` HTTP header on every request. ### What the backend should do with `X-Language` 1. **Categories & items**: If `translations` field exists for the requested language, return the translated `name`, `description`, etc. OR the backend can apply translations server-side and return already-translated fields. 2. **The `translations` field** on items (optional approach): ```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. **Recommended approach**: Read `X-Language` header and return the `name`/`description` in that language directly. If no translation exists, return the Russian default. --- ## 10. CORS Configuration For auth cookies and custom headers to work, the backend CORS config must include: ``` Access-Control-Allow-Origin: https://dexarmarket.ru (NOT 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 ``` > **Important:** `Access-Control-Allow-Origin` cannot be `*` when `Allow-Credentials: true`. Must be the exact frontend origin. **Allowed origins:** - `https://dexarmarket.ru` - `https://novo.market` - `http://localhost:4200` (dev) - `http://localhost:4201` (dev, Novo) --- ## 11. Telegram Bot Setup Each brand needs its own bot: - **Dexar:** `@dexarmarket_bot` - **Novo:** `@novomarket_bot` The bot should: 1. Listen for `/start auth_{callbackUrl}` command 2. Extract the callback URL 3. Send the user's Telegram data (`id`, `first_name`, `username`, etc.) to that callback URL 4. The callback URL is `{apiUrl}/auth/telegram/callback` --- ## Complete Endpoint Reference ### New endpoints | Method | Path | Description | Auth | |--------|---------------------------|----------------------------|----------| | `GET` | `/regions` | List available regions | No | | `GET` | `/auth/session` | Check current session | Cookie | | `GET` | `/auth/telegram/callback` | Telegram bot auth callback | No (bot) | | `POST` | `/auth/logout` | End session | Cookie | ### Existing endpoints | Method | Path | Description | Auth | Headers | |----------|-----------------------|-------------------------|------|--------------------| | `GET` | `/ping` | Health check | No | — | | `GET` | `/category` | List categories | No | X-Region, X-Language | | `GET` | `/category/:id` | Items in category | No | X-Region, X-Language | | `GET` | `/item/:id` | Single item | No | X-Region, X-Language | | `GET` | `/searchitems` | Search items | No | X-Region, X-Language | | `GET` | `/randomitems` | Random items | No | X-Region, X-Language | | `POST` | `/cart` | Add to cart / Payment | No* | — | | `PATCH` | `/cart` | Update cart quantity | No* | — | | `DELETE` | `/cart` | Remove from cart | No* | — | | `GET` | `/cart` | Get cart contents | No* | — | | `POST` | `/comment` | Submit review | No | — | | `GET` | `/qr/payment/:qrId` | Check payment status | No | — | | `POST` | `/purchase-email` | Submit email after pay | No | — | > \* Cart/payment endpoints may use the session cookie if available for order association, but don't strictly require auth. The frontend enforces auth before checkout.