HOCO B2B
SRSAPI referenceBack-office endpoints

Catalog & pricing

Back-office catalog operations — brands, category tree, filterable attributes, products and variants with their three customer-type prices, visibility, price history, product import and export.

These operations administer the B2B catalog of cahier §4.3 and §15.4: brands HOCO, ACEFAST and BOROFONE, the category tree (chargers, cables, earphones, power banks, car holders, audio, watches, accessories — with sub-categories), products and their variants, images, visibility per segment or reseller, prices and their history, and Excel/CSV import and export. Screens: products list and editor (BO-27, BO-28), brands (BO-29), categories (BO-30), the price screen (BO-31), the import wizard (BO-32) and product attributes (BO-71).

Pricing model (decision of 17 Oct 2026). Every variant has three selling prices, one per customer type — priceSuperGros, priceDemiGros, priceDetail (centimes, HT) — plus its vatRate, a recommendedPrice (prix conseillé, TTC) and the restricted purchasePrice (HT). A reseller pays the price of their customerType; promotions apply on top (PRC-01, PRC-02). Guests see no price. Segment price lists and quantity tiers are not part of V1: the admin.priceLists.* operations are reserved for the V2 per-reseller agreements (PRC-03) and admin.variants.tiers is dropped — both are documented at the end of this page as such and are not mounted by the V1 server.

Filterable attributes (CAT-12, CAT-13). Each category defines its attributes (power in W, connector, cable length, colour, battery capacity in mAh, Bluetooth version…) with admin.attributes.crud; every variant of a product in that category carries values for them, which feed the reseller filters and counts of catalog.facets.

Who uses it. The Responsable stock maintains products, categories, variants and attributes (cahier §3); the Administrateur commercial manages prices and visibility; the Super administrateur imports the initial catalog. Purchase prices and margins are visible only to holders of pricing.view (cahier §4.7 « Les droits d'accès aux données financières sont strictement limités », ACL-04).

PermissionUnlocks on this page
catalog.viewBrands, categories, attributes, products and variants (read)
catalog.create · catalog.update · catalog.deleteCreate · edit (incl. status, visibility, variants, images) · delete or archive
catalog.administerProduct import; restoring an archived product
catalog.exportCatalog export
pricing.viewpurchasePrice, margins and the price history
pricing.updateWriting any price field (priceSuperGros, priceDemiGros, priceDetail, recommendedPrice, purchasePrice, vatRate)

Settings used (settings keys): catalog.newProductDays (30 — « Nouveauté » badge), pricing.defaultVatRate (1900), stock.lowThresholdDefault.

Audit. CREATE, UPDATE, DELETE and STATUS_CHANGE rows on Brand, Category, AttributeDefinition, Product, ProductVariant, ProductVisibilityRule; IMPORT and EXPORT for the jobs. Every price change also writes a PriceChange row (history, AUD-02).

Conventions: lists return Page<T> = { items, total, page, pageSize } (responses); FileRef, Price, UserSummary are shared schemas; money in centimes, VAT in basis points; images are uploaded with files.createUpload (purpose PRODUCT_IMAGE or BRAND_LOGO, public CDN) and referenced by fileId; editable records carry version (409 VERSION_CONFLICT). 401, 429 and 500 apply everywhere and are not repeated.

MethodPathOperationAccessAppsRelease
GET · POST · PATCH · DELETE/admin/brands[/{brandId}]admin.brands.crud
Brands CRUD (delete = archive when referenced)
catalog.*BOV1
GET · POST · PATCH · DELETE/admin/categories[/{categoryId}]admin.categories.crud
Categories CRUD
catalog.*BOV1
PUT/admin/categories/orderadmin.categories.reorder
Reorder / re-parent categories
catalog.updateBOV1
GET/admin/productsadmin.products.list
Products with status, brand, category and stock filters
catalog.viewBOV1
POST/admin/productsadmin.products.create
Create a product (draft) with its default variant
catalog.createBOV1
GET/admin/products/{productId}admin.products.get
Full product for the editor (incl. purchase price if permitted)
catalog.viewBOV1
PATCH/admin/products/{productId}admin.products.update
Edit product data, specs, images order
catalog.updateBOV1
POST/admin/products/{productId}/statusadmin.products.setStatus
Publish, hide, archive
catalog.updateBOV1
PUT/admin/products/{productId}/visibilityadmin.products.visibility
Visibility rules per segment or reseller
catalog.updateBOV1
POST · PATCH · DELETE/admin/products/{productId}/variants[/{variantId}]admin.variants.crud
Variants CRUD (SKU, barcode, attributes, prices, limits)
catalog.updateBOV1
PUT/admin/variants/{variantId}/quantity-tiersadmin.variants.tiers
Quantity discount tiers — dropped from V1 (three customer-type prices instead); kept as a V2 option
pricing.updateBOV2
GET/admin/variants/{variantId}/price-historyadmin.variants.priceHistory
Price change history
pricing.viewBOV1
GET/admin/variants/pricesadmin.variants.prices
Price grid: one row per variant with its prices (BO-31)
catalog.viewBOV1
PUT/admin/variants/pricesadmin.variants.bulkPrices
Save many variants' prices in one call (PriceChange rows, per-variant failures)
pricing.updateBOV1
GET · POST · PATCH · DELETE/admin/attribute-definitions[/{attributeId}]admin.attributes.crud
Filterable attribute definitions per category (key, label, type, unit, allowed values, filter widget)
catalog.*BOV1
GET · POST · PATCH · DELETE/admin/price-lists[/{priceListId}]admin.priceLists.crud
Per-reseller price agreements (V2)
pricing.*BOV2
PUT/admin/price-lists/{priceListId}/itemsadmin.priceLists.items
Bulk upsert price list items (V2)
pricing.updateBOV2
POST/admin/imports/productsadmin.products.import
Import products from an uploaded Excel/CSV (dry-run then apply)
catalog.administerBOV1
POST/admin/products/exportadmin.products.export
Export the catalog (async)
catalog.exportBOV1

Schemas

Brand

interface Brand {
  id: Uuid;
  name: string;                 // unique: "HOCO", "ACEFAST", "BOROFONE"
  slug: string;                 // "hoco"
  logo?: FileRef;               // BRAND_LOGO, public
  description?: string;
  status: "ACTIVE" | "ARCHIVED";
  sortOrder: number;
  productCount: number;         // non-archived products
  createdAt: Iso; updatedAt: Iso; version: number;
}

Category

interface Category {
  id: Uuid;
  name: string;                 // unique among siblings
  slug: string;
  parentId: Uuid | null;        // null = top-level category; set = sub-category (max depth 2)
  path: { id: Uuid; name: string }[];   // from the root, this category included
  icon?: FileRef;
  sortOrder: number;
  status: "ACTIVE" | "ARCHIVED";
  productCount: number;         // including sub-categories
  attributeIds: Uuid[];         // attribute definitions that apply here (own + inherited from the parent)
  children?: Category[];        // tree format only
  createdAt: Iso; updatedAt: Iso; version: number;
}

AttributeDefinition

interface AttributeDefinition {
  id: Uuid;
  key: string;                  // snake_case, unique, immutable: "power_w", "connector"
  label: { fr: string; ar?: string; en?: string };
  type: "NUMBER" | "ENUM" | "BOOLEAN" | "TEXT";     // AttributeType
  unit?: string;                // NUMBER: "W", "mAh", "m"
  decimals?: number;            // NUMBER: 0–2
  values?: { code: string; label: { fr: string; ar?: string; en?: string }; sortOrder: number; active: boolean }[];   // ENUM
  multiValue: boolean;          // ENUM: a variant may carry several values (e.g. connectors of a 3-in-1 cable)
  categoryIds: Uuid[];          // applies to these categories and their sub-categories
  filterable: boolean;          // shown as a filter in the reseller apps
  filterWidget: "CHECKBOX" | "RANGE" | "TOGGLE" | "NONE";   // FilterWidget: ENUM → CHECKBOX, NUMBER → RANGE or CHECKBOX, BOOLEAN → TOGGLE
  requiredToPublish: boolean;   // every active variant needs a value before the product can be published
  minValue?: number;            // NUMBER: lowest accepted value (contract 1.1.0)
  maxValue?: number;            // NUMBER: highest accepted value (contract 1.1.0)
  showOnProductSheet: boolean;  // listed in the product sheet's specifications (contract 1.1.0); filters and variant choice keep it either way
  sortOrder: number;
  status: "ACTIVE" | "ARCHIVED";
  variantCount: number;         // variants carrying a value
  createdAt: Iso; updatedAt: Iso; version: number;
}

AdminVariant

interface AdminVariant {
  id: Uuid;
  sku: string;                  // unique, immutable once ordered: "HC-X14-CC-1M-BK"
  barcode?: string;             // EAN-13 / UPC, unique
  name: string;                 // "Noir · 1 m"
  attributes: Record<string, string | number | boolean | string[]>;   // VariantAttributeValue by attribute key
  priceSuperGros: Money;        // HT — price of SUPER_GROS resellers
  priceDemiGros: Money;         // HT — DEMI_GROS
  priceDetail: Money;           // HT — DETAIL
  pricesTtc: { superGros: Money; demiGros: Money; detail: Money };    // computed with vatRate
  vatRate: BasisPoints;
  recommendedPrice?: Money;     // prix conseillé public, TTC
  purchasePrice?: Money;        // HT — only with pricing.view
  marginsBps?: { superGros: number; demiGros: number; detail: number };   // (price − purchase) ÷ price — only with pricing.view
  minQty: number;               // ≥ 1
  qtyStep: number;              // ≥ 1; quantities must be minQty + k × qtyStep
  maxQtyPerOrder?: number;
  weightGrams?: number;
  status: "ACTIVE" | "HIDDEN" | "ARCHIVED";   // ProductStatus subset
  isDefault: boolean;
  stock: { physical: number; reserved: number; available: number; alertLevel: "OK" | "LOW" | "OUT" };   // all warehouses
  availability: "IN_STOCK" | "OUT_OF_STOCK";  // what resellers see
  createdAt: Iso; updatedAt: Iso; version: number;
}

TTC = round(HT × (10 000 + vatRate) ÷ 10 000) to the centime: 35 000 HT at 19 % → 41 650 TTC.

AdminProduct

interface AdminProduct {
  id: Uuid;
  reference: string;            // unique per brand: "HC-X14"
  name: string;
  slug: string;
  brand: { id: Uuid; name: string };
  category: { id: Uuid; name: string; parentId: Uuid | null; path: { id: Uuid; name: string }[] };
  description: string;          // plain text with line breaks, ≤ 5 000
  specs: { label: string; value: string }[];   // free technical characteristics (cahier §15.4)
  warranty?: { months: number; text?: string };
  status: ProductStatus;        // DRAFT, ACTIVE, HIDDEN, OUT_OF_STOCK, ARCHIVED
  publishedAt?: Iso;
  isNew: boolean;               // publishedAt within catalog.newProductDays
  images: { id: Uuid; file: FileRef; sortOrder: number; alt?: string; variantId?: Uuid }[];
  variants: AdminVariant[];
  defaultVariantId: Uuid;
  visibility: { mode: "EVERYONE" | "INCLUDE_ONLY" | "EXCLUDE"; segments: { id: Uuid; name: string }[]; resellers: ResellerSummary[] };
  completeness: { publishable: boolean; missing: string[] };   // e.g. ["images", "variants[0].attributes.power_w"]
  activePromotions: { id: Uuid; name: string; endsAt: Iso }[];
  createdAt: Iso; updatedAt: Iso; version: number;
}

Brands

GET /admin/brands

admin.brands.crud (list) · Permission catalog.view · Apps BO · Release V1 · Requirements CAT-01

Purpose. Lists brands (managed as data, not hard-coded — cahier §4).

Request

QueryTypeValues / defaultNotes
statusstringACTIVE | ARCHIVED; default both
qstringmin 2 charsName
sort / order / page / pageSizesortOrder (default), name, productCount · asc · 1 · 25 (max 200)

Response 200 OK

{
  "items": [
    { "id": "01928a01-1a2b-7c3d-8e4f-5a6b7c8d9e01", "name": "HOCO", "slug": "hoco", "logo": { "id": "01928a02-1a2b-7c3d-8e4f-5a6b7c8d9e02", "url": "https://cdn.hoco.dz/brands/hoco.svg", "mimeType": "image/svg+xml", "size": 4211, "purpose": "BRAND_LOGO" }, "status": "ACTIVE", "sortOrder": 1, "productCount": 412, "createdAt": "2026-10-20T08:00:00Z", "updatedAt": "2026-10-20T08:00:00Z", "version": 1 },
    { "id": "01928a01-2b3c-7d4e-9f5a-6b7c8d9e0f12", "name": "ACEFAST", "slug": "acefast", "status": "ACTIVE", "sortOrder": 2, "productCount": 138, "createdAt": "2026-10-20T08:00:00Z", "updatedAt": "2026-10-20T08:00:00Z", "version": 1 }
  ],
  "total": 3,
  "page": 1,
  "pageSize": 25
}

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks catalog.view

Rules & side effects

  1. Read-only.

Used by BO-29 · BO-27 (filter) · BO-04 (filter)

GET /admin/brands/{brandId}

admin.brands.crud (get) · Permission catalog.view · Apps BO · Release V1 · Requirements CAT-01

Purpose. One brand.

Response 200 OK — a Brand.

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks catalog.view
404NOT_FOUNDUnknown brand

Rules & side effects

  1. Read-only.

Used by BO-29

POST /admin/brands

admin.brands.crud (create) · Permission catalog.create · Apps BO · Release V1 · Requirements CAT-01

Purpose. Adds a brand.

Request

interface CreateBrandBody { name: string; logoFileId?: Uuid; description?: string; sortOrder?: number }
{ "name": "BOROFONE", "logoFileId": "01928a02-3c4d-7e5f-8a6b-7c8d9e0f1a2b", "sortOrder": 3 }

Response 201 Created — the Brand (slug borofone, status ACTIVE, productCount 0, version 1).

Errors

StatusCodeWhen
400VALIDATION_FAILEDName shorter than 2 or longer than 60 characters
403PERMISSION_DENIEDCaller lacks catalog.create
409DUPLICATE_VALUEName or slug already used
422FILE_NOT_READY · FILE_PURPOSE_MISMATCHLogo not uploaded, or purpose not BRAND_LOGO

Rules & side effects

  1. Audit CREATE. The reseller apps see the brand once it has an active product (catalog.brands).

Used by BO-29

PATCH /admin/brands/{brandId}

admin.brands.crud (update) · Permission catalog.update · Apps BO · Release V1 · Requirements CAT-01

Purpose. Edits a brand, or restores an archived one.

Request

interface UpdateBrandBody { name?: string; logoFileId?: Uuid | null; description?: string | null; sortOrder?: number; status?: "ACTIVE" | "ARCHIVED"; version: number }
{ "sortOrder": 1, "version": 1 }

Response 200 OK — the updated Brand.

Errors

StatusCodeWhen
400VALIDATION_FAILEDInvalid name
403PERMISSION_DENIEDCaller lacks catalog.update
404NOT_FOUNDUnknown brand
409DUPLICATE_VALUE · VERSION_CONFLICTName taken · stale version

Rules & side effects

  1. Archiving a brand (status: ARCHIVED) hides it from brand filters; its products keep their own status (archive them explicitly). Audit UPDATE.

Used by BO-29

DELETE /admin/brands/{brandId}

admin.brands.crud (delete) · Permission catalog.delete · Apps BO · Release V1 · Requirements CAT-01

Purpose. Deletes a brand — or archives it when products reference it.

Response 200 OK

{ "id": "01928a01-9f0a-7b1c-8d2e-3f4a5b6c7d8e", "result": "ARCHIVED", "references": [{ "entityType": "Product", "count": 3 }] }

result is DELETED (no product ever referenced the brand) or ARCHIVED.

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks catalog.delete
404NOT_FOUNDUnknown brand

Rules & side effects

  1. Audit DELETE (or STATUS_CHANGE when archived). Past orders and invoices keep the brand name in their snapshot.

Used by BO-29

Categories

GET /admin/categories

admin.categories.crud (list) · Permission catalog.view · Apps BO · Release V1 · Requirements CAT-02

Purpose. The category tree (categories and sub-categories) or a flat list.

Request

QueryTypeValues / defaultNotes
formatstringtree (default) | flattree is not paginated
statusstringACTIVE | ARCHIVED; default both
qstringmin 2 charsflat only

Response 200 OK — format=tree

{
  "items": [
    {
      "id": "01928b01-1a2b-7c3d-8e4f-5a6b7c8d9e01", "name": "Câbles", "slug": "cables", "parentId": null,
      "path": [{ "id": "01928b01-1a2b-7c3d-8e4f-5a6b7c8d9e01", "name": "Câbles" }],
      "sortOrder": 2, "status": "ACTIVE", "productCount": 186,
      "attributeIds": ["0192b100-3c4d-7e5f-9a6b-7c8d9e0f1a2b", "0192b100-4d5e-7f6a-8b7c-8d9e0f1a2b3c", "0192b100-5e6f-7a7b-9c8d-9e0f1a2b3c4d"],
      "createdAt": "2026-10-20T08:10:00Z", "updatedAt": "2026-10-20T08:10:00Z", "version": 1,
      "children": [
        {
          "id": "01928b02-2b3c-7d4e-9f5a-6b7c8d9e0f12", "name": "Câbles USB-C", "slug": "cables-usb-c", "parentId": "01928b01-1a2b-7c3d-8e4f-5a6b7c8d9e01",
          "path": [{ "id": "01928b01-1a2b-7c3d-8e4f-5a6b7c8d9e01", "name": "Câbles" }, { "id": "01928b02-2b3c-7d4e-9f5a-6b7c8d9e0f12", "name": "Câbles USB-C" }],
          "sortOrder": 1, "status": "ACTIVE", "productCount": 74,
          "attributeIds": ["0192b100-3c4d-7e5f-9a6b-7c8d9e0f1a2b", "0192b100-4d5e-7f6a-8b7c-8d9e0f1a2b3c", "0192b100-5e6f-7a7b-9c8d-9e0f1a2b3c4d", "0192b100-1a2b-7c3d-9e4f-5a6b7c8d9e01"],
          "createdAt": "2026-10-20T08:11:00Z", "updatedAt": "2026-10-20T08:11:00Z", "version": 1, "children": []
        }
      ]
    }
  ],
  "total": 31
}

Errors

StatusCodeWhen
400VALIDATION_FAILEDUnknown format
403PERMISSION_DENIEDCaller lacks catalog.view

Rules & side effects

  1. Read-only. total counts all nodes. format=flat returns Page<Category> without children.

Used by BO-30 · BO-27 · BO-28 · BO-71

GET /admin/categories/{categoryId}

admin.categories.crud (get) · Permission catalog.view · Apps BO · Release V1 · Requirements CAT-02

Purpose. One category with its children and the attribute definitions that apply to it.

Response 200 OK — a Category with children and an extra attributes: AttributeDefinition[] (own and inherited).

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks catalog.view
404NOT_FOUNDUnknown category

Rules & side effects

  1. Read-only.

Used by BO-30 · BO-71

POST /admin/categories

admin.categories.crud (create) · Permission catalog.create · Apps BO · Release V1 · Requirements CAT-02

Purpose. Creates a category or a sub-category.

Request

interface CreateCategoryBody { name: string; parentId?: Uuid | null; iconFileId?: Uuid; sortOrder?: number }
{ "name": "Câbles Lightning", "parentId": "01928b01-1a2b-7c3d-8e4f-5a6b7c8d9e01", "sortOrder": 2 }

Response 201 Created — the Category; it inherits the parent's attributes.

Errors

StatusCodeWhen
400VALIDATION_FAILEDName shorter than 2 or longer than 60 characters
403PERMISSION_DENIEDCaller lacks catalog.create
404NOT_FOUNDUnknown parent
409DUPLICATE_VALUESame name under the same parent
422CATEGORY_DEPTH_EXCEEDEDThe parent is already a sub-category (max depth 2: category › sub-category)

Rules & side effects

  1. Audit CREATE. Empty categories are not shown to resellers (catalog.categories counts visible products).

Used by BO-30

PATCH /admin/categories/{categoryId}

admin.categories.crud (update) · Permission catalog.update · Apps BO · Release V1 · Requirements CAT-02

Purpose. Renames, re-icons, archives or restores a category (moving it is done with reorder).

Request

interface UpdateCategoryBody { name?: string; iconFileId?: Uuid | null; sortOrder?: number; status?: "ACTIVE" | "ARCHIVED"; version: number }
{ "name": "Câbles USB-C et Thunderbolt", "version": 1 }

Response 200 OK — the updated Category.

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks catalog.update
404NOT_FOUNDUnknown category
409DUPLICATE_VALUE · VERSION_CONFLICTSibling with the same name · stale version
409RESOURCE_IN_USEArchiving a category that still has active products or active children

Rules & side effects

  1. Audit UPDATE.

Used by BO-30

DELETE /admin/categories/{categoryId}

admin.categories.crud (delete) · Permission catalog.delete · Apps BO · Release V1 · Requirements CAT-02

Purpose. Deletes an empty category.

Response 204 No Content

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks catalog.delete
404NOT_FOUNDUnknown category
409RESOURCE_IN_USEIt has sub-categories or products (any status) — details.references; move them or archive the category

Rules & side effects

  1. Attribute definitions linked only to this category stay (they can be re-linked). Audit DELETE.

Used by BO-30

PUT /admin/categories/order

admin.categories.reorder · Permission catalog.update · Apps BO · Release V1 · Requirements CAT-02

Purpose. Saves the drag-and-drop result of the category editor: order and parent of every node.

Request

interface ReorderCategoriesBody {
  items: { id: Uuid; parentId: Uuid | null; sortOrder: number }[];   // the whole tree
}
{
  "items": [
    { "id": "01928b01-0a1b-7c2d-8e3f-4a5b6c7d8e9f", "parentId": null, "sortOrder": 1 },
    { "id": "01928b01-1a2b-7c3d-8e4f-5a6b7c8d9e01", "parentId": null, "sortOrder": 2 },
    { "id": "01928b02-2b3c-7d4e-9f5a-6b7c8d9e0f12", "parentId": "01928b01-1a2b-7c3d-8e4f-5a6b7c8d9e01", "sortOrder": 1 }
  ]
}

Response 200 OK — the tree as in admin.categories.crud (format=tree).

Errors

StatusCodeWhen
400VALIDATION_FAILEDA category missing from items or listed twice, unknown id
403PERMISSION_DENIEDCaller lacks catalog.update
422CATEGORY_CYCLEA node would be its own ancestor
422CATEGORY_DEPTH_EXCEEDEDA node would sit under a sub-category, or a category with children would become a sub-category

Rules & side effects

  1. All-or-nothing. Re-parenting changes the inherited attributes of the moved node: values of attributes that no longer apply stay stored but are ignored by filters, and the response lists them in warnings: [{ categoryId, orphanAttributeKeys }].
  2. Audit one UPDATE row per moved node.

Used by BO-30

Products

GET /admin/products

admin.products.list · Permission catalog.view · Apps BO · Release V1 · Requirements CAT-03

Purpose. The products table with status, brand, category, stock and price filters.

Request

QueryTypeValues / defaultNotes
statusProductStatus[]repeatable
brandIdUuid[]repeatable
categoryIdUuid[]repeatableIncludes sub-categories
availabilitystringIN_STOCK | OUT_OF_STOCKAs resellers see it
alertLevelstring[]OK | LOW | OUTWorst level among variants and warehouses
visibilitystringEVERYONE | INCLUDE_ONLY | EXCLUDE
incompletebooleantrue: cannot be published yet (completeness.publishable = false)
hasPromotion / isNewboolean
attr.<key>stringe.g. attr.connector=USB_C, attr.power_w=20..65Attribute filters (ENUM: values separated by ,; NUMBER: min..max)
priceDetailMin / priceDetailMaxMoneyHT, on the default variant
qstringmin 2 charsName, reference, SKU, barcode (exact)
sortstringupdatedAt (default), name, reference, publishedAt, available, priceDetail
order / page / pageSizeasc | desc (default desc) · 1 · 25 (max 200)

Response 200 OK

{
  "items": [
    {
      "id": "0192b215-6f70-7a81-9b2c-3d4e5f6a7b80",
      "reference": "AF-A29",
      "name": "Chargeur GaN 50 W 2 ports",
      "brand": { "id": "01928a01-2b3c-7d4e-9f5a-6b7c8d9e0f12", "name": "ACEFAST" },
      "category": { "id": "01928b01-0a1b-7c2d-8e3f-4a5b6c7d8e9f", "name": "Chargeurs", "parentId": null },
      "status": "ACTIVE",
      "imageUrl": "https://cdn.hoco.dz/p/af-a29/main-400.webp",
      "variantsCount": 2,
      "defaultVariant": { "id": "0192b215-7a8b-7c9d-8e0f-1a2b3c4d5e6f", "sku": "AF-A29-50W-WH", "priceSuperGros": 195000, "priceDemiGros": 210000, "priceDetail": 230000, "vatRate": 1900 },
      "stock": { "physical": 46, "reserved": 40, "available": 6, "alertLevel": "LOW" },
      "availability": "IN_STOCK",
      "visibilityMode": "EVERYONE",
      "isNew": true,
      "hasPromotion": true,
      "completeness": { "publishable": true, "missing": [] },
      "publishedAt": "2026-10-22T09:00:00Z",
      "updatedAt": "2026-11-02T14:31:00Z"
    }
  ],
  "total": 583,
  "page": 1,
  "pageSize": 25
}

Errors

StatusCodeWhen
400VALIDATION_FAILEDUnknown status, sort field or attribute key, malformed range
403PERMISSION_DENIEDCaller lacks catalog.view

Rules & side effects

  1. Read-only. Stock figures sum all warehouses (per-warehouse detail in admin.stock.levels); quantities exist only in the back-office (STK-07).

Used by BO-27 · BO-31 · BO-50 (scope picker) · BO-40 (product picker)

POST /admin/products

admin.products.create · Permission catalog.create (+ pricing.update for price fields) · Apps BO · Release V1 · Requirements CAT-03

Purpose. Creates a product in DRAFT with its default variant.

Request

interface CreateProductBody {
  reference: string;                 // unique per brand, 2–40, uppercase letters, digits and "-"
  name: string;                      // 3–150
  brandId: Uuid;
  categoryId: Uuid;                  // a sub-category when the category has children
  description?: string;
  specs?: { label: string; value: string }[];
  warranty?: { months: number; text?: string };
  defaultVariant: {
    sku: string; barcode?: string; name?: string;
    attributes?: Record<string, string | number | boolean | string[]>;
    priceSuperGros?: Money; priceDemiGros?: Money; priceDetail?: Money;   // HT; need pricing.update
    vatRate?: BasisPoints;           // default pricing.defaultVatRate
    recommendedPrice?: Money; purchasePrice?: Money;
    minQty?: number; qtyStep?: number; maxQtyPerOrder?: number; weightGrams?: number;
  };
}
{
  "reference": "HC-X14",
  "name": "Câble USB-C vers USB-C 60 W 1 m",
  "brandId": "01928a01-1a2b-7c3d-8e4f-5a6b7c8d9e01",
  "categoryId": "01928b02-2b3c-7d4e-9f5a-6b7c8d9e0f12",
  "description": "Câble tressé nylon, charge rapide PD jusqu'à 60 W, transfert de données 480 Mb/s.",
  "specs": [{ "label": "Gaine", "value": "Nylon tressé" }, { "label": "Débit", "value": "480 Mb/s" }],
  "warranty": { "months": 6 },
  "defaultVariant": {
    "sku": "HC-X14-CC-1M-BK", "barcode": "6931474761234", "name": "Noir · 1 m",
    "attributes": { "connector": ["USB_C"], "cable_length_m": 1, "power_w": 60, "color": "BLACK" },
    "priceSuperGros": 32000, "priceDemiGros": 35000, "priceDetail": 39000, "vatRate": 1900,
    "recommendedPrice": 55000, "purchasePrice": 21000, "minQty": 5, "qtyStep": 5, "maxQtyPerOrder": 500, "weightGrams": 38
  }
}

Response 201 Created — the AdminProduct with status DRAFT, one variant, completeness: { "publishable": false, "missing": ["images"] }, version 1.

Errors

StatusCodeWhen
400VALIDATION_FAILEDBad reference format, value of the wrong type for an attribute, unknown ENUM code, qtyStep < 1
403PERMISSION_DENIEDCaller lacks catalog.create, or sends price fields without pricing.update — details: { required: "pricing.update", fields: ["priceDetail"] }
404NOT_FOUNDUnknown brand or category
409DUPLICATE_VALUEReference (per brand), SKU or barcode already used
422PRICE_ORDER_INVALIDPrices not ordered priceSuperGros ≤ priceDemiGros ≤ priceDetail

Rules & side effects

  1. A product is invisible to resellers until published (setStatus). Attribute keys must belong to the category's attributes.
  2. Audit CREATE on Product and ProductVariant; one PriceChange row per price field set (before: null).

Used by BO-28

GET /admin/products/{productId}

admin.products.get · Permission catalog.view · Apps BO · Release V1 · Requirements CAT-03 · ACL-04

Purpose. The full product for the editor: data, images, variants with their three prices and attributes, visibility, completeness — purchase price and margins only if permitted.

Response 200 OK — AdminProduct

{
  "id": "0192b210-4d5e-7f60-8a1b-2c3d4e5f6a70",
  "reference": "HC-X14",
  "name": "Câble USB-C vers USB-C 60 W 1 m",
  "slug": "hoco-hc-x14-cable-usb-c-60w-1m",
  "brand": { "id": "01928a01-1a2b-7c3d-8e4f-5a6b7c8d9e01", "name": "HOCO" },
  "category": { "id": "01928b02-2b3c-7d4e-9f5a-6b7c8d9e0f12", "name": "Câbles USB-C", "parentId": "01928b01-1a2b-7c3d-8e4f-5a6b7c8d9e01", "path": [{ "id": "01928b01-1a2b-7c3d-8e4f-5a6b7c8d9e01", "name": "Câbles" }, { "id": "01928b02-2b3c-7d4e-9f5a-6b7c8d9e0f12", "name": "Câbles USB-C" }] },
  "description": "Câble tressé nylon, charge rapide PD jusqu'à 60 W, transfert de données 480 Mb/s.",
  "specs": [{ "label": "Gaine", "value": "Nylon tressé" }, { "label": "Débit", "value": "480 Mb/s" }],
  "warranty": { "months": 6 },
  "status": "ACTIVE",
  "publishedAt": "2026-10-21T10:00:00Z",
  "isNew": true,
  "images": [
    { "id": "0192b210-6a7b-7c8d-9e0f-1a2b3c4d5e6f", "file": { "id": "0192b210-7b8c-7d9e-8f0a-1b2c3d4e5f60", "url": "https://cdn.hoco.dz/p/hc-x14/main.webp", "thumbnailUrl": "https://cdn.hoco.dz/p/hc-x14/main-400.webp", "mimeType": "image/webp", "size": 186410, "purpose": "PRODUCT_IMAGE" }, "sortOrder": 1, "alt": "Câble HOCO X14 noir" }
  ],
  "variants": [
    {
      "id": "0192b210-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
      "sku": "HC-X14-CC-1M-BK",
      "barcode": "6931474761234",
      "name": "Noir · 1 m",
      "attributes": { "connector": ["USB_C"], "cable_length_m": 1, "power_w": 60, "color": "BLACK" },
      "priceSuperGros": 32000,
      "priceDemiGros": 35000,
      "priceDetail": 39000,
      "pricesTtc": { "superGros": 38080, "demiGros": 41650, "detail": 46410 },
      "vatRate": 1900,
      "recommendedPrice": 55000,
      "purchasePrice": 21000,
      "marginsBps": { "superGros": 3438, "demiGros": 4000, "detail": 4615 },
      "minQty": 5,
      "qtyStep": 5,
      "maxQtyPerOrder": 500,
      "weightGrams": 38,
      "status": "ACTIVE",
      "isDefault": true,
      "stock": { "physical": 1240, "reserved": 160, "available": 1080, "alertLevel": "OK" },
      "availability": "IN_STOCK",
      "createdAt": "2026-10-21T09:40:00Z",
      "updatedAt": "2026-10-28T11:02:00Z",
      "version": 3
    }
  ],
  "defaultVariantId": "0192b210-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
  "visibility": { "mode": "EVERYONE", "segments": [], "resellers": [] },
  "completeness": { "publishable": true, "missing": [] },
  "activePromotions": [],
  "createdAt": "2026-10-21T09:40:00Z",
  "updatedAt": "2026-10-28T11:02:00Z",
  "version": 5
}

Field notes. purchasePrice and marginsBps — only with pricing.view; omitted otherwise (not null). Margins: (32 000 − 21 000) ÷ 32 000 = 34,38 %; (35 000 − 21 000) ÷ 35 000 = 40 %; (39 000 − 21 000) ÷ 39 000 = 46,15 %.

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks catalog.view
404NOT_FOUNDUnknown product

Rules & side effects

  1. Read-only.

Used by BO-28 · BO-31

PATCH /admin/products/{productId}

admin.products.update · Permission catalog.update · Apps BO · Release V1 · Requirements CAT-03

Purpose. Edits product data, specifications, warranty, category and the ordered image gallery.

Request

interface UpdateProductBody {
  name?: string;
  reference?: string;                // only while no variant has been ordered
  brandId?: Uuid;
  categoryId?: Uuid;
  description?: string;
  specs?: { label: string; value: string }[];   // replaces the list
  warranty?: { months: number; text?: string } | null;
  images?: { id?: Uuid; fileId?: Uuid; sortOrder: number; alt?: string; variantId?: Uuid | null }[];   // full ordered gallery: keep by id, add by fileId; omitted ids are removed
  defaultVariantId?: Uuid;
  version: number;
}
{
  "images": [
    { "id": "0192b210-6a7b-7c8d-9e0f-1a2b3c4d5e6f", "sortOrder": 2 },
    { "fileId": "0193020a-1b2c-7d3e-8f4a-5b6c7d8e9f01", "sortOrder": 1, "alt": "Câble HOCO X14 enroulé" }
  ],
  "version": 5
}

Response 200 OK — the updated AdminProduct (version 6).

Errors

StatusCodeWhen
400VALIDATION_FAILEDMore than 12 images, duplicate sortOrder, image variantId of another product
403PERMISSION_DENIEDCaller lacks catalog.update
404NOT_FOUNDUnknown product, brand, category or image
409DUPLICATE_VALUE · VERSION_CONFLICTReference taken · stale version
409RESOURCE_IN_USEreference change after a variant was ordered
422FILE_NOT_READY · FILE_PURPOSE_MISMATCHImage not uploaded or not PRODUCT_IMAGE
422PRODUCT_INCOMPLETEThe change would leave an ACTIVE product without image (unpublish first)

Rules & side effects

  1. Images are public CDN files; the first by sortOrder is the main image. Removed images are kept 30 days for orders' snapshots, then purged.
  2. A category change re-checks attribute values: values of attributes that do not apply to the new category are kept but flagged in warnings.
  3. Audit UPDATE with the changed fields. Reseller catalog caches are invalidated (ETag change).

Used by BO-28

POST /admin/products/{productId}/status

admin.products.setStatus · Permission catalog.update (catalog.administer to restore an archived product) · Apps BO · Release V1 · Requirements CAT-04

Purpose. Publishes, hides, shows again, archives or restores a product.

Request

interface SetProductStatusBody { to: "ACTIVE" | "HIDDEN" | "ARCHIVED"; reason?: string; version: number }
{ "to": "ACTIVE", "version": 1 }

Response 200 OK — the AdminProduct with its new status (ACTIVE, or OUT_OF_STOCK when every variant has 0 available).

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks catalog.update (or catalog.administer for ARCHIVED → HIDDEN)
404NOT_FOUNDUnknown product
409PRODUCT_TRANSITION_NOT_ALLOWEDMove not in Lifecycles › Product — e.g. to: OUT_OF_STOCK (system only), ARCHIVED → ACTIVE
409VERSION_CONFLICTStale version
422PRODUCT_INCOMPLETEPublishing without the minimum data of cahier §15.4: brand, reference, name, category, description, ≥ 1 image, VAT rate, ≥ 1 active variant with SKU and the three prices, values for every requiredToPublish attribute — details: { missing: [] }

Rules & side effects

  1. DRAFT/HIDDEN → ACTIVE sets publishedAt on first publication (drives « Nouveautés »); ACTIVE/OUT_OF_STOCK → HIDDEN; any → ARCHIVED; ARCHIVED → HIDDEN (restore). OUT_OF_STOCK ↔ ACTIVE is automatic from stock.
  2. Hidden or archived products disappear from catalog, search, favorites and promotions at the next read; cart lines become blocking issues PRODUCT_UNAVAILABLE.
  3. Audit STATUS_CHANGE with the reason.
  4. Notifications: none by default (a « Nouveauté » push is a promotion or announcement decision).

Used by BO-28 · BO-27

PUT /admin/products/{productId}/visibility

admin.products.visibility · Permission catalog.update · Apps BO · Release V1 · Requirements CAT-05

Purpose. Sets who may see and order a product: everyone, only some segments or resellers, or everyone except them (cahier §4.3 « visibilité par revendeur ou segment »).

Request

interface ProductVisibilityBody {
  mode: "EVERYONE" | "INCLUDE_ONLY" | "EXCLUDE";
  segmentIds: Uuid[];                // ≤ 50
  resellerIds: Uuid[];               // ≤ 500
  version: number;
}
{ "mode": "INCLUDE_ONLY", "segmentIds": ["01929a10-1b2c-7d3e-8f4a-5b6c7d8e9f01"], "resellerIds": [], "version": 6 }

Response 200 OK — { "productId": "…", "mode": "INCLUDE_ONLY", "segments": [{ "id": "01929a10-1b2c-7d3e-8f4a-5b6c7d8e9f01", "name": "Grands comptes Ouest" }], "resellers": [], "audienceCount": 64, "version": 7 }

Errors

StatusCodeWhen
400VALIDATION_FAILEDINCLUDE_ONLY/EXCLUDE with empty lists, EVERYONE with non-empty lists
403PERMISSION_DENIEDCaller lacks catalog.update
404NOT_FOUNDUnknown product, segment or reseller
409VERSION_CONFLICTStale version

Rules & side effects

  1. Stored as ProductVisibilityRule rows; applied by every reseller-facing read (catalog, search, facets, product page, cart, checkout) and by assisted orders. Guests see only EVERYONE products.
  2. Audit UPDATE with the before/after audience.

Used by BO-28

Variants and prices

POST /admin/products/{productId}/variants

admin.variants.crud (create) · Permission catalog.update (+ pricing.update for price fields) · Apps BO · Release V1 · Requirements CAT-03 · PRC-01

Purpose. Adds a variant (colour, length, power…) to a product, with its SKU, barcode, attribute values, three prices and quantity limits.

Request

interface CreateVariantBody {
  sku: string;                       // 3–60, uppercase, digits, "-"
  barcode?: string;
  name: string;
  attributes: Record<string, string | number | boolean | string[]>;
  priceSuperGros: Money; priceDemiGros: Money; priceDetail: Money;   // HT
  vatRate?: BasisPoints;
  recommendedPrice?: Money;          // TTC
  purchasePrice?: Money;             // HT
  minQty?: number;                   // default 1
  qtyStep?: number;                  // default 1
  maxQtyPerOrder?: number;
  weightGrams?: number;
  status?: "ACTIVE" | "HIDDEN";      // default ACTIVE
  alertThreshold?: number;           // initial stock alert threshold (default stock.lowThresholdDefault)
  priceChangeReason?: string;
}
{
  "sku": "HC-X14-CC-2M-BK", "barcode": "6931474761241", "name": "Noir · 2 m",
  "attributes": { "connector": ["USB_C"], "cable_length_m": 2, "power_w": 60, "color": "BLACK" },
  "priceSuperGros": 38000, "priceDemiGros": 41000, "priceDetail": 45000, "vatRate": 1900,
  "recommendedPrice": 65000, "purchasePrice": 25000, "minQty": 5, "qtyStep": 5
}

Response 201 Created — the AdminVariant (pricesTtc: { superGros: 45220, demiGros: 48790, detail: 53550 }, stock zero, availability OUT_OF_STOCK, version 1).

Errors

StatusCodeWhen
400VALIDATION_FAILEDBad SKU, attribute not defined for the category or of the wrong type, maxQtyPerOrder < minQty
403PERMISSION_DENIEDCaller lacks catalog.update, or pricing.update for the price fields
404NOT_FOUNDUnknown product
409DUPLICATE_VALUESKU or barcode already used; another variant of the product has exactly the same attribute values
422PRICE_ORDER_INVALIDpriceSuperGros ≤ priceDemiGros ≤ priceDetail not respected

Rules & side effects

  1. StockLevel rows are created at 0 in every active warehouse. A product in ACTIVE stays ACTIVE (the new variant is simply out of stock).
  2. Audit CREATE; PriceChange rows for the initial prices (source: MANUAL).

Used by BO-28

PATCH /admin/products/{productId}/variants/{variantId}

admin.variants.crud (update) · Permission catalog.update (+ pricing.update for price fields) · Apps BO · Release V1 · Requirements CAT-03 · PRC-01

Purpose. Edits a variant — including its prices, whose every change is historised.

Request

interface UpdateVariantBody extends Partial<Omit<CreateVariantBody, "alertThreshold">> {
  status?: "ACTIVE" | "HIDDEN" | "ARCHIVED";
  isDefault?: boolean;
  priceChangeReason?: string;        // stored on the PriceChange rows (recommended for price changes)
  version: number;
}
{ "priceDemiGros": 36000, "priceDetail": 40000, "priceChangeReason": "Hausse fournisseur de 5 % au 1er novembre.", "version": 3 }

Response 200 OK — the updated AdminVariant (pricesTtc.demiGros 42 840, pricesTtc.detail 47 600, version 4).

Errors

StatusCodeWhen
400VALIDATION_FAILEDAs for creation
403PERMISSION_DENIEDMissing catalog.update or pricing.update — details.fields lists the refused price fields
404NOT_FOUNDUnknown product or variant
409DUPLICATE_VALUESKU or barcode already used
409RESOURCE_IN_USESKU change on an ordered variant
409VARIANT_LAST_ACTIVEHiding or archiving the last active variant of an ACTIVE product (hide the product instead)
409VERSION_CONFLICTStale version
422PRICE_ORDER_INVALIDPrice order not respected

Rules & side effects

  1. New prices apply from the next pricing: carts are re-priced at their next read (issue PRICE_CHANGED), submitted orders keep their snapshot; an order waiting for confirmation shows the difference in its price control (admin.orders.get).
  2. One PriceChange row per changed price field (PRICE_SUPER_GROS, PRICE_DEMI_GROS, PRICE_DETAIL, RECOMMENDED_PRICE, PURCHASE_PRICE, VAT_RATE) with before/after, actor and reason; audit UPDATE.
  3. Notifications: none (price changes reach resellers through the catalog).

Used by BO-28 · BO-31

DELETE /admin/products/{productId}/variants/{variantId}

admin.variants.crud (delete) · Permission catalog.update · Apps BO · Release V1 · Requirements CAT-03

Purpose. Deletes a variant that was never ordered or moved in stock; otherwise archives it.

Response 200 OK — { "id": "0192b210-8c9d-7e0f-9a1b-2c3d4e5f6a7b", "result": "ARCHIVED", "references": [{ "entityType": "OrderItem", "count": 12 }] } (result: DELETED or ARCHIVED)

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks catalog.update
404NOT_FOUNDUnknown product or variant
409VARIANT_LAST_ACTIVELast active variant of an ACTIVE product
409RESOURCE_IN_USEPhysical stock is not 0 in some warehouse (adjust or transfer first)

Rules & side effects

  1. Audit DELETE or STATUS_CHANGE. Price history is kept for archived variants.
  2. An archived variant answers status: ARCHIVED (SRV-42 — stored apart from HIDDEN); both are inactive and not orderable, and archived variants leave the price grid (admin.variants.prices). A PATCH with status: ACTIVE or HIDDEN restores it.

Used by BO-28

PUT /admin/variants/{variantId}/quantity-tiers

admin.variants.tiers · Permission pricing.update · Apps BO · Release Removed from V1 · Requirements PRC-02

Dropped from V1 (decision of 17 Oct 2026)

Quantity discount tiers are replaced by the three customer-type prices. The V1 server does not mount this route (404 NOT_FOUND); endpoints.json still lists it and should be updated. Volume incentives in V1 are promotions with a minimum quantity (admin.promotions.crud).

Purpose. Reserved: per-variant quantity tiers, should HOCO ask for them again.

Request — reserved contract: { "tiers": [{ "minQty": 50, "discountBps": 300 }], "version": 4 }.

Response — not served in V1.

Errors

StatusCodeWhen
404NOT_FOUNDAlways in V1 (route not mounted)

Rules & side effects

  1. None in V1. QuantityTier is not part of the V1 data model.

Used by — (no V1 screen)

GET /admin/variants/{variantId}/price-history

admin.variants.priceHistory · Permission pricing.view · Apps BO · Release V1 · Requirements PRC-01 · AUD-02

Purpose. The history of every price of a variant (cahier §4.3, §8 « historique des modifications »).

Request

QueryTypeValues / defaultNotes
fieldstring[]PRICE_SUPER_GROS | PRICE_DEMI_GROS | PRICE_DETAIL | RECOMMENDED_PRICE | PURCHASE_PRICE | VAT_RATEPriceChangeField, repeatable
from / tostring (date)
page / pageSizenumber1 / 25 (max 200)Sorted by changedAt desc

Response 200 OK

{
  "items": [
    { "id": "01930212-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "field": "PRICE_DETAIL", "before": 39000, "after": 40000, "changedAt": "2026-11-01T07:30:00Z", "changedBy": { "id": "01929e55-2c3d-7e4f-9a1b-2c3d4e5f6a7b", "displayName": "Nadia Belkacem" }, "reason": "Hausse fournisseur de 5 % au 1er novembre.", "source": "MANUAL" },
    { "id": "01930212-2b3c-7d4e-9f5a-6b7c8d9e0f1a", "field": "PRICE_DEMI_GROS", "before": 35000, "after": 36000, "changedAt": "2026-11-01T07:30:00Z", "changedBy": { "id": "01929e55-2c3d-7e4f-9a1b-2c3d4e5f6a7b", "displayName": "Nadia Belkacem" }, "reason": "Hausse fournisseur de 5 % au 1er novembre.", "source": "MANUAL" }
  ],
  "total": 8,
  "page": 1,
  "pageSize": 25
}

Field notes. source: MANUAL, IMPORT (product import, with the import id in importId), SYSTEM (VAT migration). Amounts are HT except RECOMMENDED_PRICE (TTC); VAT_RATE values are basis points.

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks pricing.view
404NOT_FOUNDUnknown variant

Rules & side effects

  1. Read-only; PriceChange rows are append-only.

Used by BO-28 · BO-31

GET /admin/variants/prices

admin.variants.prices · Permission catalog.view (costs: pricing.view + finance.view) · Apps BO · Release V1 · Requirements PRC-02 · PRC-01 · Added in contract 1.1.0 (SRV-41)

Purpose. The price grid of BO-31: one row per variant with its three customer-type prices, its VAT, recommended price and — for callers who may see costs — its purchase price and margins, plus the latest price change.

Request

QueryTypeValues / defaultNotes
page / pageSizenumber1 / 25 (max 200)
qstring≥ 2 charactersProduct name or reference, variant SKU, barcode (exact)
brandId / categoryIdUuid[]repeatableThe category includes its sub-categories
statusProductStatus[]repeatable; default every status but ARCHIVEDProduct status
variantStatusstring[]ACTIVE | HIDDEN
missingstringprice | purchasePriceA selling price at 0 / no purchase price
marginBelownumberbasis pointsDétail margin below the value — 403 without pricing.view + finance.view
changedSincestring (date)A PriceChange on or after the day (Africa/Algiers)
sortstringproductName (default) | sku | priceSuperGros | priceDemiGros | priceDetail | lastChangedAt
orderstringasc (default) | desc

Response 200 OK

{
  "items": [
    {
      "variantId": "0192e0b1-6a7b-7c8d-9e0f-1a2b3c4d5e6f", "productId": "0192e0b1-5f6a-7b8c-9d0e-1f2a3b4c5d6e",
      "productName": "Chargeur rapide 20 W USB-C", "productReference": "ACF-A21", "productStatus": "ACTIVE",
      "variantName": "Noir", "sku": "ACF-A21-BK", "status": "ACTIVE",
      "brand": { "id": "0192d001-…", "name": "ACEFAST" }, "category": { "id": "0192d101-…", "name": "Chargeurs secteur" },
      "priceSuperGros": 38000, "priceDemiGros": 42000, "priceDetail": 48000, "vatRate": 1900, "recommendedPrice": 69000,
      "purchasePrice": 30000, "marginsBps": { "superGros": 2105, "demiGros": 2857, "detail": 3750 },
      "lastChange": { "changedAt": "2026-11-01T07:30:00Z", "changedBy": { "id": "01929e55-…", "displayName": "Nadia Belkacem" } },
      "updatedAt": "2026-11-01T07:30:00Z", "version": 1793518200000
    }
  ],
  "total": 412, "page": 1, "pageSize": 25
}

Errors

StatusCodeWhen
400VALIDATION_FAILEDUnknown sort or filter value
403PERMISSION_DENIEDCaller lacks catalog.view; marginBelow without pricing.view + finance.view

Used by BO-31

PUT /admin/variants/prices

admin.variants.bulkPrices · Permission catalog.update + pricing.update (purchase price: also finance.view) · Apps BO · Release V1 · Requirements PRC-02 · PRC-01 · AUD-02 · Added in contract 1.1.0 (SRV-41)

Purpose. Saves a whole price grid in one call: the changed prices of up to 500 variants, each with its version, under one reason.

Request

{
  "reason": "Hausse fournisseur de 5 % au 1er novembre.",
  "items": [
    { "variantId": "0192e0b1-6a7b-7c8d-9e0f-1a2b3c4d5e6f", "version": 1793518200000, "priceDemiGros": 44100, "priceDetail": 50400 },
    { "variantId": "0192e0b1-7b8c-7d9e-8f0a-1b2c3d4e5f60", "version": 1793518200000, "priceSuperGros": 39900 }
  ]
}

Each item carries only the prices that change: priceSuperGros, priceDemiGros, priceDetail, recommendedPrice (TTC), purchasePrice. The VAT rate is edited on the variant.

Response 200 OK

{
  "updated": 1,
  "priceChanges": 2,
  "items": [ { "variantId": "0192e0b1-6a7b-7c8d-9e0f-1a2b3c4d5e6f", "priceDemiGros": 44100, "priceDetail": 50400, "…": "a VariantPriceRow, re-read" } ],
  "failed": [ { "variantId": "0192e0b1-7b8c-7d9e-8f0a-1b2c3d4e5f60", "sku": "ACF-A21-WH", "code": "PRICE_ORDER_INVALID", "message": "Prices must be ordered super-gros ≤ demi-gros ≤ détail" } ]
}

Errors (the whole call)

StatusCodeWhen
400VALIDATION_FAILEDNo item, more than 500, a variant twice, no price in any item, reason shorter than 3 characters
403PERMISSION_DENIEDCaller lacks catalog.update or pricing.update (finance.view for purchasePrice) — details: { required, fields }

Per-variant failures (in failed, the other variants are saved): NOT_FOUND (unknown variant), VERSION_CONFLICT (stale version), PRICE_ORDER_INVALID (super-gros ≤ demi-gros ≤ détail broken after the change), VALIDATION_FAILED (a selling price at 0).

Rules & side effects

  1. Each variant is saved in its own transaction (optimistic lock on version): one PriceChange row per changed price with the reason and the caller — the same rows as admin.variants.crud — the product version bumped, and an audit UPDATE on ProductVariant.
  2. A drop of a customer-type price of smart.priceDropMinPercent or more emits price.dropped (SN-21), as for a single edit.
  3. Prices apply to the next carts; placed orders keep their snapshot.

Used by BO-31

Attributes

Attribute definitions make the advanced reseller filters possible (power, connector, cable length, colour, capacity, Bluetooth version…, CAT-12, CAT-13). A definition is linked to categories; it applies to their sub-categories; each variant stores its values (VariantAttributeValue). Filters and counts are served to the apps by catalog.facets.

GET /admin/attribute-definitions

admin.attributes.crud (list) · Permission catalog.view · Apps BO · Release V1 · Requirements CAT-13

Purpose. Lists attribute definitions, optionally those applying to one category.

Request

QueryTypeValues / defaultNotes
categoryIdUuidDefinitions applying to it (own and inherited)
statusstringACTIVE (default) | ARCHIVED
filterableboolean
qstringmin 2 charsKey or French label
sort / order / page / pageSizesortOrder (default), key · asc · 1 · 50 (max 200)

Response 200 OK

{
  "items": [
    {
      "id": "0192b100-1a2b-7c3d-9e4f-5a6b7c8d9e01", "key": "power_w", "label": { "fr": "Puissance", "ar": "القدرة", "en": "Power" },
      "type": "NUMBER", "unit": "W", "decimals": 0, "multiValue": false,
      "categoryIds": ["01928b01-0a1b-7c2d-8e3f-4a5b6c7d8e9f", "01928b02-2b3c-7d4e-9f5a-6b7c8d9e0f12"],
      "filterable": true, "filterWidget": "RANGE", "requiredToPublish": true, "sortOrder": 1, "status": "ACTIVE",
      "variantCount": 311, "createdAt": "2026-10-23T08:00:00Z", "updatedAt": "2026-10-23T08:00:00Z", "version": 1
    },
    {
      "id": "0192b100-3c4d-7e5f-9a6b-7c8d9e0f1a2b", "key": "connector", "label": { "fr": "Connecteur", "en": "Connector" },
      "type": "ENUM", "multiValue": true,
      "values": [
        { "code": "USB_C", "label": { "fr": "USB-C" }, "sortOrder": 1, "active": true },
        { "code": "LIGHTNING", "label": { "fr": "Lightning" }, "sortOrder": 2, "active": true },
        { "code": "MICRO_USB", "label": { "fr": "Micro-USB" }, "sortOrder": 3, "active": true },
        { "code": "USB_A", "label": { "fr": "USB-A" }, "sortOrder": 4, "active": true }
      ],
      "categoryIds": ["01928b01-1a2b-7c3d-8e4f-5a6b7c8d9e01"],
      "filterable": true, "filterWidget": "CHECKBOX", "requiredToPublish": true, "sortOrder": 2, "status": "ACTIVE",
      "variantCount": 402, "createdAt": "2026-10-23T08:05:00Z", "updatedAt": "2026-10-24T10:12:00Z", "version": 2
    }
  ],
  "total": 9,
  "page": 1,
  "pageSize": 50
}

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks catalog.view
404NOT_FOUNDUnknown categoryId

Rules & side effects

  1. Read-only.

Used by BO-71 · BO-28 (variant attribute form)

GET /admin/attribute-definitions/{attributeId}

admin.attributes.crud (get) · Permission catalog.view · Apps BO · Release V1 · Requirements CAT-13

Purpose. One definition, with the distribution of its values (for clean-up before editing allowed values).

Response 200 OK — an AttributeDefinition plus valueCounts, e.g. { "USB_C": 288, "LIGHTNING": 97, "MICRO_USB": 41, "USB_A": 112 }.

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks catalog.view
404NOT_FOUNDUnknown definition

Rules & side effects

  1. Read-only.

Used by BO-71

POST /admin/attribute-definitions

admin.attributes.crud (create) · Permission catalog.create · Apps BO · Release V1 · Requirements CAT-13

Purpose. Defines a filterable attribute for one or more categories.

Request

interface CreateAttributeBody {
  key: string;                       // ^[a-z][a-z0-9_]{1,39}$, unique
  label: { fr: string; ar?: string; en?: string };
  type: "NUMBER" | "ENUM" | "BOOLEAN" | "TEXT";
  unit?: string; decimals?: number;
  values?: { code: string; label: { fr: string; ar?: string; en?: string }; sortOrder?: number }[];   // ENUM: 1–100 values, codes UPPER_SNAKE_CASE
  multiValue?: boolean;              // ENUM only
  categoryIds: Uuid[];               // ≥ 1
  filterable?: boolean;              // default true (TEXT: always false)
  filterWidget?: "CHECKBOX" | "RANGE" | "TOGGLE" | "NONE";
  requiredToPublish?: boolean;       // default false
  minValue?: number; maxValue?: number;   // NUMBER only, min ≤ max (contract 1.1.0)
  showOnProductSheet?: boolean;      // default true (contract 1.1.0)
  sortOrder?: number;
}
{
  "key": "capacity_mah", "label": { "fr": "Capacité", "en": "Capacity" }, "type": "NUMBER", "unit": "mAh", "decimals": 0,
  "minValue": 1000, "maxValue": 50000, "showOnProductSheet": true,
  "categoryIds": ["01928b01-3c4d-7e5f-9a6b-7c8d9e0f1a2b"], "filterable": true, "filterWidget": "RANGE", "requiredToPublish": true
}

The update body takes the same fields; minValue / maxValue accept null to clear a bound. A variant value outside the range is refused when a variant is created or edited and in a product import (400 VALIDATION_FAILED, field code OUT_OF_RANGE on attributes.<key>). showOnProductSheet: false keeps the attribute in the filters and the variant choice; the reseller product detail flags it with onProductSheet: false so the sheet leaves it out of the specifications.

Response 201 Created — the AttributeDefinition (variantCount 0, version 1).

Errors

StatusCodeWhen
400VALIDATION_FAILEDBad key, values on a non-ENUM type, widget incompatible with the type (RANGE on ENUM, TOGGLE on NUMBER), TEXT marked filterable, a range on a non-NUMBER type or minValue > maxValue
403PERMISSION_DENIEDCaller lacks catalog.create
404NOT_FOUNDUnknown category
409DUPLICATE_VALUEKey already used; duplicate ENUM code

Rules & side effects

  1. requiredToPublish applies to products published from now on; already active products lacking the value are listed by admin.products.list ?incomplete=true but stay published.
  2. Audit CREATE on AttributeDefinition. Facet caches of the categories are invalidated.

Used by BO-71

PATCH /admin/attribute-definitions/{attributeId}

admin.attributes.crud (update) · Permission catalog.update · Apps BO · Release V1 · Requirements CAT-13

Purpose. Edits labels, allowed values, categories, filter settings, or archives a definition.

Request

interface UpdateAttributeBody {
  label?: { fr: string; ar?: string; en?: string };
  unit?: string | null; decimals?: number;
  values?: { code: string; label: { fr: string; ar?: string; en?: string }; sortOrder?: number; active?: boolean }[];   // full list; codes cannot be removed while used — deactivate them
  categoryIds?: Uuid[];
  filterable?: boolean; filterWidget?: "CHECKBOX" | "RANGE" | "TOGGLE" | "NONE";
  requiredToPublish?: boolean; sortOrder?: number;
  status?: "ACTIVE" | "ARCHIVED";
  version: number;
}
{ "values": [
  { "code": "USB_C", "label": { "fr": "USB-C" }, "sortOrder": 1 },
  { "code": "LIGHTNING", "label": { "fr": "Lightning" }, "sortOrder": 2 },
  { "code": "MICRO_USB", "label": { "fr": "Micro-USB" }, "sortOrder": 3 },
  { "code": "USB_A", "label": { "fr": "USB-A" }, "sortOrder": 4 },
  { "code": "MAGSAFE", "label": { "fr": "MagSafe" }, "sortOrder": 5 }
], "version": 2 }

Response 200 OK — the updated AttributeDefinition.

Errors

StatusCodeWhen
400VALIDATION_FAILEDAs for creation; key and type are immutable
403PERMISSION_DENIEDCaller lacks catalog.update
404NOT_FOUNDUnknown definition or category
409RESOURCE_IN_USERemoving an ENUM code that variants use — details: { code, variantCount }
409VERSION_CONFLICTStale version

Rules & side effects

  1. Archiving hides the filter from the apps and keeps stored values. Audit UPDATE; facet caches invalidated.

Used by BO-71

DELETE /admin/attribute-definitions/{attributeId}

admin.attributes.crud (delete) · Permission catalog.delete · Apps BO · Release V1 · Requirements CAT-13

Purpose. Deletes an unused definition (otherwise archive it).

Response 204 No Content

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks catalog.delete
404NOT_FOUNDUnknown definition
409RESOURCE_IN_USEAt least one variant carries a value — archive instead

Rules & side effects

  1. Audit DELETE.

Used by BO-71

Price lists and agreements (V2)

Not in V1 (decision of 17 Oct 2026)

The three customer-type prices replace segment price lists in V1. These operations are reserved for the V2 per-reseller agreements and level-based prices (PRC-03) and are not mounted by the V1 server (404 NOT_FOUND). endpoints.json still tags them V1 and describes them as segment price lists; it should be updated. BO-31 becomes, in V1, the three-price grid built on admin.products.list and admin.variants.crud.

V2 baseline: an agreement (PriceList, type: AGREEMENT) binds one or more resellers to specific unit prices (PriceListItem: variant + unit price HT, or a discount in basis points on the customer-type price), with validFrom/validTo and a priority. Resolution in V2: active agreement of the reseller → customer-type price → promotions on top.

interface PriceList {                       // V2
  id: Uuid;
  name: string;
  type: "AGREEMENT" | "LEVEL";              // V2: per-reseller agreement, or a loyalty-level list
  resellers: ResellerSummary[];             // AGREEMENT
  loyaltyLevels?: LoyaltyLevel[];           // LEVEL
  validFrom?: string; validTo?: string;     // YYYY-MM-DD, inclusive
  priority: number;                         // higher wins when several apply
  status: "ACTIVE" | "INACTIVE";
  itemCount: number;
  createdAt: Iso; updatedAt: Iso; version: number;
}

GET /admin/price-lists

admin.priceLists.crud (list) · Permission pricing.view · Apps BO · Release V2 · Requirements PRC-03

V2

Contract reserved; not built in V1 (cahier §14.2 and the 17 Oct 2026 decision).

Purpose. Lists agreements and level price lists.

Request — filters type, status, resellerId, validAt (date), q; sort name | validFrom | priority; page / pageSize.

Response 200 OK — Page<PriceList>, e.g. { "items": [{ "id": "0194a001-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "name": "Accord Phone Store El Bahia 2027", "type": "AGREEMENT", "resellers": [{ "id": "0192f4b8-61c2-7a3e-9b10-1f2e3d4c5b6a", "number": "REV-000123", "storeName": "Phone Store El Bahia", "ownerName": "Karim Benali", "phone": "+213555123456", "wilayaCode": "31", "communeName": "Bir El Djir", "status": "ACTIVE" }], "validFrom": "2027-01-01", "validTo": "2027-12-31", "priority": 10, "status": "ACTIVE", "itemCount": 42, "createdAt": "2026-12-15T09:00:00Z", "updatedAt": "2026-12-15T09:00:00Z", "version": 1 }], "total": 1, "page": 1, "pageSize": 25 }.

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks pricing.view
404NOT_FOUNDV1 (route not mounted)

Rules & side effects

  1. Read-only.

Used by BO-31 (V2)

GET /admin/price-lists/{priceListId}

admin.priceLists.crud (get) · Permission pricing.view · Apps BO · Release V2 · Requirements PRC-03

V2

Contract reserved; not built in V1.

Purpose. One price list with a page of its items (?page, ?pageSize, ?q on SKU or name).

Response 200 OK — the PriceList plus items: Page<{ variantId, sku, productName, customerTypePrice: Money, unitPrice?: Money, discountBps?: number }>.

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks pricing.view
404NOT_FOUNDUnknown price list (always in V1)

Rules & side effects

  1. Read-only.

Used by BO-31 (V2)

POST /admin/price-lists

admin.priceLists.crud (create) · Permission pricing.create · Apps BO · Release V2 · Requirements PRC-03

V2

Contract reserved; not built in V1.

Purpose. Creates an agreement or a level price list.

Request — { name, type, resellerIds?, loyaltyLevels?, validFrom?, validTo?, priority }, e.g. { "name": "Accord Phone Store El Bahia 2027", "type": "AGREEMENT", "resellerIds": ["0192f4b8-61c2-7a3e-9b10-1f2e3d4c5b6a"], "validFrom": "2027-01-01", "validTo": "2027-12-31", "priority": 10 }.

Response 201 Created — the PriceList.

Errors

StatusCodeWhen
400VALIDATION_FAILEDvalidTo before validFrom, no reseller on an agreement
403PERMISSION_DENIEDCaller lacks pricing.create
409DUPLICATE_VALUEName taken

Rules & side effects

  1. Audit CREATE; PriceChange rows (PRICE_LIST_ITEM) for each item later set.

Used by BO-31 (V2)

PATCH /admin/price-lists/{priceListId}

admin.priceLists.crud (update) · Permission pricing.update · Apps BO · Release V2 · Requirements PRC-03

V2

Contract reserved; not built in V1.

Purpose. Edits validity, priority, assigned resellers or status of a price list.

Request — partial of the creation body plus status and version.

Response 200 OK — the updated PriceList.

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks pricing.update
404NOT_FOUNDUnknown price list
409VERSION_CONFLICTStale version

Rules & side effects

  1. Audit UPDATE; carts of the affected resellers re-priced at their next read.

Used by BO-31 (V2)

DELETE /admin/price-lists/{priceListId}

admin.priceLists.crud (delete) · Permission pricing.delete · Apps BO · Release V2 · Requirements PRC-03

V2

Contract reserved; not built in V1.

Purpose. Deletes a price list never used by an order; otherwise deactivates it.

Response 200 OK — { "id": "…", "result": "DELETED" } or "INACTIVE".

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks pricing.delete
404NOT_FOUNDUnknown price list

Rules & side effects

  1. Audit DELETE or STATUS_CHANGE.

Used by BO-31 (V2)

PUT /admin/price-lists/{priceListId}/items

admin.priceLists.items · Permission pricing.update · Apps BO · Release V2 · Requirements PRC-03

V2

Contract reserved; not built in V1.

Purpose. Bulk upsert of a price list's items.

Request — { "mode": "MERGE" | "REPLACE", "items": [{ "sku": "AF-A29-50W-WH", "unitPrice": 200000 }, { "sku": "HC-X14-CC-1M-BK", "discountBps": 500 }], "removeSkus": [], "version": 1 } — at most 2 000 items; each item has exactly one of unitPrice (HT) or discountBps.

Response 200 OK — { "upserted": 2, "removed": 0, "itemCount": 44, "version": 2 }

Errors

StatusCodeWhen
400VALIDATION_FAILEDUnknown SKU, both or neither of unitPrice / discountBps — all-or-nothing, details.fields per index
403PERMISSION_DENIEDCaller lacks pricing.update
404NOT_FOUNDUnknown price list
409VERSION_CONFLICTStale version

Rules & side effects

  1. One PriceChange row (PRICE_LIST_ITEM) per changed item; audit UPDATE.

Used by BO-31 (V2)

Import and export

POST /admin/imports/products

admin.products.import · Permission catalog.administer (+ pricing.update to import price columns) · Apps BO · Release V1 · Requirements CAT-06

Purpose. Imports or updates products and variants from an uploaded Excel/CSV file: a dry run first produces a row-by-row report, then an apply run writes the valid rows (cahier §4.3 « Import et export des produits via Excel/CSV », §13.2 initial import). The job is an ImportJob of type PRODUCTS, polled with admin.imports.get.

Request

interface ProductImportBody {
  mode: "DRY_RUN" | "APPLY";
  fileId?: Uuid;                     // DRY_RUN: completed upload, purpose IMPORT_FILE
  dryRunId?: Uuid;                   // APPLY: the successful dry run to apply
  options?: {
    updateExisting?: boolean;        // default true — rows whose SKU exists update the variant (and its product)
    createMissingBrands?: boolean;   // default false
    createMissingCategories?: boolean;   // default false
    targetStatus?: "DRAFT" | "ACTIVE";   // default DRAFT for new products; ACTIVE only when complete
    images?: "URLS" | "NONE";        // default URLS: download image_urls (https, ≤ 5 MB each)
  };
}
{ "mode": "DRY_RUN", "fileId": "01930300-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "options": { "updateExisting": true, "targetStatus": "DRAFT" } }

Template columns (first sheet of an XLSX, or CSV UTF-8 with ; or ,; header row required; * = required; amounts in DA with , or . decimals, converted to centimes):

ColumnExampleNotes
brand*HOCOExisting brand name (or created with createMissingBrands)
category*Câbles > Câbles USB-CPath with >
reference*HC-X14Groups rows of the same product
name*Câble USB-C vers USB-C 60 W 1 m
description
specsGaine=Nylon tressé; Débit=480 Mb/slabel=value pairs separated by ;
warranty_months6
sku*HC-X14-CC-1M-BKNatural key of the row
barcode6931474761234
variant_nameNoir · 1 m
attr:<key>attr:power_w = 60, attr:connector = USB_C,LIGHTNINGOne column per attribute key of the category
price_super_gros_ht* · price_demi_gros_ht* · price_detail_ht*320,00 · 350,00 · 390,00Need pricing.update
recommended_price_ttc550,00
purchase_price_ht210,00Needs pricing.update
vat_rate19Percent; default pricing.defaultVatRate
min_qty · qty_step · max_qty_per_order · weight_g5 · 5 · 500 · 38
statusDRAFTDRAFT, ACTIVE or HIDDEN
image_urlshttps://…/1.jpg, https://…/2.jpgComma-separated

Response 202 Accepted — the ImportJob

{
  "id": "01930300-2b3c-7d4e-9f5a-6b7c8d9e0f1a",
  "type": "PRODUCTS",
  "mode": "DRY_RUN",
  "status": "QUEUED",
  "file": { "id": "01930300-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "url": "https://files.hoco.dz/s/catalogue-hoco-nov.xlsx?sig=…", "mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "size": 812400, "purpose": "IMPORT_FILE" },
  "options": { "updateExisting": true, "createMissingBrands": false, "createMissingCategories": false, "targetStatus": "DRAFT", "images": "URLS" },
  "createdBy": { "id": "01929e66-3d4e-7f5a-8b6c-7d8e9f0a1b2c", "displayName": "Sofiane Hamdi" },
  "createdAt": "2026-11-04T07:30:00Z"
}

When it finishes, admin.imports.get returns e.g. summary: { "totalRows": 1240, "validRows": 1198, "warningRows": 30, "errorRows": 12, "toCreate": 1105, "toUpdate": 123, "unchanged": 0 } and rows such as { "row": 57, "status": "ERROR", "action": "CREATE", "key": "BF-BH20-WH", "messages": [{ "column": "price_detail_ht", "code": "PRICE_ORDER_INVALID", "message": "Le prix détail doit être ≥ au prix demi-gros." }] }.

Errors

StatusCodeWhen
400VALIDATION_FAILEDfileId with APPLY, dryRunId with DRY_RUN
403PERMISSION_DENIEDCaller lacks catalog.administer; price columns present without pricing.update
404NOT_FOUNDUnknown file or dry run
409IMPORT_DRY_RUN_EXPIREDThe dry run is older than 24 h
409IMPORT_ALREADY_APPLIEDThe dry run was already applied
422IMPORT_FILE_INVALIDUnreadable file, missing required columns, more than 5 000 rows — details: { missingColumns, maxRows }
422IMPORT_DRY_RUN_REQUIREDAPPLY on a dry run that did not succeed
422FILE_NOT_READY · FILE_PURPOSE_MISMATCHUpload not completed or not IMPORT_FILE

Rules & side effects

  1. The dry run validates every row exactly like the single operations (references, SKUs, barcodes, attribute values, price order, quantity rules) and writes nothing. APPLY re-validates and writes VALID and WARNING rows only, in batches of 200; rows that became invalid are SKIPPED.
  2. Prices imported write PriceChange rows with source: IMPORT; stock is not imported here (stock import).
  3. Audit IMPORT on apply (counts in summary). Notifications: import.finished → requester (in-app + email); SSE import.finished.

Used by BO-32

POST /admin/products/export

admin.products.export · Permission catalog.export · Apps BO · Release V1 · Requirements CAT-06 · ANA-05

Purpose. Exports the filtered catalog — one row per variant, in the import template's columns so the file can be edited and re-imported.

Request

interface ExportProductsBody {
  format: "XLSX" | "CSV" | "PDF";    // PDF = printable price catalogue (no purchase price)
  filters: Record<string, unknown>;  // GET /admin/products query
  columns?: string[];                // default: the import template columns the caller may see
  includeStock?: boolean;            // adds physical / reserved / available per warehouse
}
{ "format": "XLSX", "filters": { "brandId": ["01928a01-2b3c-7d4e-9f5a-6b7c8d9e0f12"], "status": ["ACTIVE", "OUT_OF_STOCK"] }, "includeStock": true }

Response 202 Accepted — an ExportJob with "source": { "kind": "LIST", "key": "products" }, "status": "QUEUED".

Errors

StatusCodeWhen
400VALIDATION_FAILEDUnknown column or filter
403PERMISSION_DENIEDCaller lacks catalog.export; purchase_price_ht requested without pricing.view
422EXPORT_TOO_LARGEAbove 100 000 rows (2 000 for PDF)
429RATE_LIMITEDAlready 3 running exports

Rules & side effects

  1. purchase_price_ht is included only for holders of pricing.view (omitted from the default columns otherwise). Audit EXPORT. Notification and SSE export.ready to the requester.

Used by BO-27 · BO-60

On this page