Products API

Manage categories, brands, products, product images, product inventory, and product pricing. Public (no auth): list products, product detail, categories, brands, stock status, pricing. Protected routes require Authorization: Bearer <access_token> and permissions: manage_products (categories, brands, products, images, pricing), manage_inventory (create inventory, restock, logs; see Inventory API). Currencies and taxes: Currencies API, Taxes API. Base path: <API_BASE_URL>/products.

Categories

All category routes require manage_products. Query active_only: use 1, true, or yes to filter active only.

GET /products/categories

List categories. Response: {"categories": [{"id", "name", "slug", "description", "is_active", "created_at"}, ...]}.

POST /products/categories

Create category. Body: name (required), description (optional), is_active (optional, default true). Slug is auto-generated from name. 409 if name exists.

GET /products/categories/<category_id>

Get one category. Public. 404 if not found.

PATCH /products/categories/<category_id>

Update category. Body: optional name, description, is_active. Slug is updated from name. 409 if name used by another.

DELETE /products/categories/<category_id>

Delete category. Products with this category have category_id set to null. 404 if not found.

Brands

Same query and response shape as categories.

GET /products/brands

List brands. Response: {"brands": [{"id", "name", "slug", "description", "is_active", "created_at"}, ...]}.

POST /products/brands

Create brand. Body: name (required), description (optional), is_active (optional). Slug auto-generated. 409 if name exists.

GET /products/brands/<brand_id>

Get one brand. Public. 404 if not found.

PATCH /products/brands/<brand_id>

Update brand. Body: optional name, description, is_active.

DELETE /products/brands/<brand_id>

Delete brand. Products with this brand have brand_id set to null. 404 if not found.

Products

Product list and detail include category, brand, images, and image_urls (main + others) using the request host as base URL. List and detail require view_products. Create, update, delete require manage_products.

GET /products/products

List products. Permission: view_products. Query: active_only (optional). Response: {"products": [{"id", "sku", "name", "description", "category_id", "brand_id", "is_active", "created_at", "updated_at", "category", "brand", "images", "image_urls"}, ...]}.

POST /products/products

Create product. Permission: manage_products. Body: name (required), sku (optional; auto-generated if omitted, format PRD-YYMMDD-XXXXXX), description, category_id, brand_id, is_active (optional, default true). 409 if SKU already exists. Category/brand must be active if provided.

GET /products/products/<product_id>

Get one product with category, brand, images, and image_urls. Public. 404 if not found.

PATCH /products/products/<product_id>

Update product. Body: only include fields to change: sku, name, description, category_id, brand_id, is_active. Omitted fields are left unchanged. 409 if SKU used by another product.

DELETE /products/products/<product_id>

Delete product and all its images (DB and files). 404 if not found.

Stock status and restock

Create inventory for a product, view stock status (customers can check availability), restock, and view history. See Inventory API for low-stock, out-of-stock, and needs-reorder reports.

POST /products/products/<product_id>/inventory

Create inventory for a product. Permission: manage_inventory. Body: optional stock_quantity, reserved_quantity, track_inventory, allow_backorder, low_stock_threshold, reorder_point, reorder_quantity. 409 if inventory already exists. Response: full inventory object with product (201).

GET /products/products/<product_id>/inventory

Get stock status for a product. Permission: view_products. Response: {"id", "product_id", "stock_quantity", "reserved_quantity", "available_quantity", "track_inventory", "allow_backorder", "low_stock_threshold", "reorder_point", "reorder_quantity", "is_in_stock", "is_low_stock", "is_out_of_stock", "needs_reorder", "product": {"id", "sku", "name"}, ...}. 404 if no inventory.

POST /products/products/<product_id>/restock

Restock a product. Permission: manage_inventory. Body: quantity (required), optional reason (e.g. manual_restock, supplier_delivery, customer_return), reference. Creates an inventory log entry. Returns updated inventory (200). 400 if invalid quantity.

GET /products/products/<product_id>/inventory/logs

Get stock history for a product. Permission: manage_inventory. Query: limit (default 50, max 100), offset (default 0). Response: {"logs": [{"id", "action", "reason", "quantity", "previous_quantity", "new_quantity", "reference", "created_at"}, ...]}.

Create and update pricing

Product pricing uses currencies and taxes. Configure them first via Currencies API and Taxes API. GET is public. Create, update, delete sale require manage_products.

POST /products/products/<product_id>/pricing

Create pricing for a product. Body: price (required), optional currency_code, compare_at_price, cost_price, tax_id, is_taxable (default true). 409 if pricing already exists.

GET /products/products/<product_id>/pricing

Get pricing detail. Response: product_id, price, compare_at_price, cost_price, currency, is_on_sale, discount_percentage, savings, tax, profit, formatted_price.

PUT /products/products/<product_id>/pricing

Update product price. Body: price (required), optional compare_at_price.

DELETE /products/products/<product_id>/pricing/sale

Remove sale price. Response: full pricing detail (200).

Upload and delete images

All image routes require manage_products.

POST /products/products/<product_id>/images

Upload an image. Use multipart/form-data. Form fields: file or image (required), image_type (optional): main for main image, or a number for position. Allowed types: png, jpg, jpeg, gif, webp. Max size 5MB. Returns {"id", "product_id", "filename", "is_main", "position"} (201). 400 if no file or invalid file.

DELETE /products/products/<product_id>/images/<image_id>

Delete one image. Image must belong to the given product. 404 if image not found or product_id mismatch.

Common responses

401 if missing or invalid token. 403 if user lacks the required permission (view_products, manage_products, manage_inventory). 404 if resource not found. 409 if name or SKU already exists, or inventory or pricing already exists for product.