# API Documentation Endpoint reference for the Marketplace Backoffice. Base URL: `https://your-api-domain.com/api` > 🇷🇺 Документация на русском языке: [API.ru.md](./API.ru.md) --- ## Projects ### Get All Projects ``` GET /api/projects Response 200: [ { "id": "dexar", "name": "dexar", "displayName": "Dexar Marketplace", "active": true, "logoUrl": "https://..." } ] ``` --- ## Categories ### Get Categories for Project ``` GET /api/projects/:projectId/categories Response 200: [ { "id": "cat1", "name": "Electronics", "visible": true, "priority": 1, "img": "https://...", "projectId": "dexar", "subcategories": [ ...Subcategory[] ] } ] ``` ### Get Single Category ``` GET /api/categories/:categoryId Response 200: { "id": "cat1", "name": "Electronics", "visible": true, "priority": 1, "img": "https://...", "projectId": "dexar", "subcategories": [ ...Subcategory[] ] } ``` ### Create Category ``` POST /api/projects/:projectId/categories Body: { "name": "New Category", // required "visible": true, "priority": 10, "img": "https://..." } Response 201: (created category object) ``` ### Update Category ``` PATCH /api/categories/:categoryId Body: (any subset of fields) { "name": "Updated Name", "visible": false, "priority": 5 } Response 200: (updated category object) ``` ### Delete Category ``` DELETE /api/categories/:categoryId Response 204 No Content Note: Cascades and deletes all subcategories and items under this category ``` --- ## Subcategories Subcategories are **recursive** - they can be nested under a root `Category` or under another `Subcategory`. The nesting depth is unlimited, with one constraint: **a subcategory that already contains items cannot have child subcategories** (and vice versa). ### Subcategory Object ```json { "id": "sub1", "name": "Smartphones", "visible": true, "priority": 1, "img": "https://...", "categoryId": "cat1", // always the ROOT category ID "parentId": "cat1", // direct parent ID (category OR subcategory) "itemCount": 15, "hasItems": true, "subcategories": [] // nested children (empty when hasItems = true) } ``` > `categoryId` is always the **root-level category** this subtree belongs to. > `parentId` is the **direct parent** - it can be either a category ID or a subcategory ID. --- ### Get Subcategories Under a Category ``` GET /api/categories/:categoryId/subcategories Response 200: Subcategory[] (nested subcategories populated recursively) ``` ### Get Single Subcategory ``` GET /api/subcategories/:subcategoryId Response 200: Subcategory object (with nested subcategories if any) ``` ### Create Subcategory Under a Category (level 1) ``` POST /api/categories/:categoryId/subcategories Body: { "id": "custom-id", // optional, auto-generated if omitted (used in URL routing) "name": "Smartphones", // required "visible": true, "priority": 10 } Response 201: (created subcategory object) Error 400: if category does not exist ``` ### Create Subcategory Under a Parent Subcategory (level 2+, nested) ``` POST /api/subcategories/:parentSubcategoryId/subcategories Body: { "id": "custom-id", // optional, auto-generated if omitted (used in URL routing) "name": "Apple", // required "visible": true, "priority": 10 } Response 201: (created subcategory object) Error 400: if parent subcategory has items (hasItems = true) Error 404: if parent subcategory does not exist ``` ### Update Subcategory ``` PATCH /api/subcategories/:subcategoryId Body: (any subset of fields) { "id": "new-slug", // ID is editable - used for marketplace URL routing "name": "Updated Name", "visible": false, "priority": 3 } Response 200: (updated subcategory object) ``` ### Delete Subcategory ``` DELETE /api/subcategories/:subcategoryId Response 204 No Content Note: Cascades and deletes all nested subcategories and items under this subcategory ``` --- ## Items Items always belong to the **deepest subcategory** in the hierarchy (a leaf node). A subcategory with at least one item has `hasItems: true` and cannot have child subcategories. ### Get Items (Paginated) ``` GET /api/subcategories/:subcategoryId/items Query Params: page number (default: 1) limit number (default: 20) search string optional - filters by name (case-insensitive) visible boolean optional - filter by visibility tags string optional - comma-separated tag list Response 200: { "items": [ { "id": "item1", "name": "iPhone 15 Pro", "visible": true, "priority": 1, "quantity": 50, "price": 1299, "currency": "USD", "imgs": ["https://...", "https://..."], "tags": ["new", "featured"], "badges": ["new", "exclusive"], "simpleDescription": "Latest iPhone...", "description": [ { "key": "Color", "value": "Black" }, { "key": "Storage", "value": "256GB" } ], "subcategoryId": "sub1", "translations": { "ru": { "name": "iPhone 15 Про", "simpleDescription": "Последний iPhone...", "description": [ { "key": "Цвет", "value": "Чёрный" }, { "key": "Память", "value": "256 ГБ" } ] } }, "comments": [ { "id": "c1", "text": "Great product!", "author": "John Doe", "stars": 5, "createdAt": "2024-01-10T10:30:00Z" } ] } ], "total": 150, "page": 1, "limit": 20, "hasMore": true } ``` ### Get Single Item ``` GET /api/items/:itemId Response 200: (full item object) ``` ### Create Item ``` POST /api/subcategories/:subcategoryId/items Body: { "name": "New Product", // required "visible": true, "priority": 10, "quantity": 100, "price": 999, "currency": "USD", // USD | EUR | RUB | GBP | UAH "imgs": ["https://..."], "tags": ["new"], "badges": ["new", "exclusive"], // optional - predefined or custom badge labels "simpleDescription": "Short description", "description": [ { "key": "Size", "value": "Large" } ], "translations": { // optional - localized content for marketplace "ru": { "name": "Новый товар", "simpleDescription": "Краткое описание", "description": [ { "key": "Размер", "value": "Большой" } ] } } } Response 201: (created item object) Side effect: Sets hasItems = true on the subcategory. The subcategory can no longer receive child subcategories. ``` ### Update Item ``` PATCH /api/items/:itemId Body: (any subset of fields) { "name": "Updated Name", "price": 899, "quantity": 80, "visible": false } Response 200: (updated item object) ``` **Updating images - always send the full array:** ``` PATCH /api/items/:itemId Body: { "imgs": ["https://new1.jpg", "https://new2.jpg"] } Response 200: (updated item object) ``` ### Delete Item ``` DELETE /api/items/:itemId Response 204 No Content Side effect: If no items remain in the subcategory, sets hasItems = false. The subcategory can then receive child subcategories again. ``` ### Bulk Update Items ``` PATCH /api/items/bulk Body: { "itemIds": ["item1", "item2", "item3"], "data": { "visible": true } } Response 204 No Content ``` --- ## Upload ### Upload Image ``` POST /api/upload Body: multipart/form-data image: File Response 201: { "url": "https://cdn.example.com/uploads/abc123.jpg" } ``` --- ## Notes - All responses are JSON. - Use `PATCH` for partial updates - send only the fields you want to change. - `priority`: lower number = appears first in the list. - `currency` supported values: `USD`, `EUR`, `RUB`, `GBP`, `UAH`. - `badges`: optional string array. Predefined values with UI colors: `new`, `sale`, `exclusive`, `hot`, `limited`, `bestseller`, `featured`. Custom strings are also allowed. - `imgs`: always send the **complete** array on update, not individual images. - `description`: array of `{ key, value }` pairs - free-form attributes per item. - `translations`: optional object keyed by language code (`"ru"`, `"en"`, etc.) — each value may contain `name`, `simpleDescription`, `description[]`. The marketplace frontend should use these when rendering in the corresponding language, falling back to the default fields if a translation is absent. - Auto-save from the backoffice fires `PATCH` with a single field every ~500 ms. --- ## Business Rules ### Nested Subcategories The hierarchy works like this: ``` Category (e.g. Electronics) Subcategory L1 (e.g. Kitchen) <- can add more children OR items, not both Subcategory L2 (e.g. Big Kitchen) <- same rule Subcategory L3 (e.g. Ovens) <- if this has items, it is a leaf Items... ``` Rules: - A category can always receive new subcategories (categories never hold items directly). - A subcategory that **has items** (`hasItems: true`) **cannot** receive child subcategories. - `POST /api/subcategories/:id/subcategories` on a node with `hasItems: true` -> `400 Bad Request`. - A subcategory that **has children** cannot have items added to it (items only go to leaf nodes). - When the **first item** is created in a subcategory -> `hasItems` becomes `true`. - When the **last item** is deleted -> `hasItems` becomes `false`; child subcategories can be added again. ### URL Structure (Marketplace Frontend) Subcategory and item `id` fields are used directly in the marketplace URL path: ``` /{categoryId}/{sub1Id}/{sub2Id}/.../{itemId} Examples: /electronics/smartphones/iphone-15 /electronics/smartphones/apple/iphone-15-pro /furniture/living-room/sofas/corner-sofa-modelo ``` The `id` field on subcategories is editable via `PATCH` to allow renaming slugs. ### Comments - `stars` is optional, integer 1-5. - `author` is optional (anonymous comments allowed). - `createdAt` is ISO 8601 string. ### Cascade Deletes | Delete target | Also deletes | |---|---| | Category | all subcategories (recursive) and their items | | Subcategory | all nested subcategories (recursive) and their items | | Item | nothing else | --- ## Internationalization (i18n) The API supports localized content for items and categories via the `translations` field. ### `translations` object structure Any entity that supports translations accepts the same nested shape: ```json { "translations": { "ru": { "name": "Название", "simpleDescription": "Краткое описание", "description": [ { "key": "Цвет", "value": "Чёрный" } ] } } } ``` - Keys are ISO 639-1 language codes (`"ru"`, `"en"`, etc.). - All sub-fields are optional — omit what you don't need. - Currently supported in the backoffice: `ru` (Russian). ### Requesting a Specific Language Pass `?lang=ru` to any GET endpoint. The backend will: 1. Return the default top-level fields as-is. 2. Merge the matching `translations[lang]` values into the response, overwriting the default fields. 3. Fall back gracefully to the default language if no translation exists for the requested lang. ``` GET /api/items/:itemId?lang=ru GET /api/subcategories/:subcategoryId/items?lang=ru ``` Alternatively, you can use the `Accept-Language` header: ``` Accept-Language: ru ``` ### Fallback Behaviour | Scenario | Returned value | |---|---| | Translation exists for requested lang | Translated value | | Translation missing for requested lang | Default (base) field value | | `lang` param omitted | Default (base) field value | > 🇷🇺 See [API.ru.md](./API.ru.md) for full documentation in Russian.