Skip to content

Products API

Admin API

All admin endpoints require JWT authentication or an API key with products.* permissions.

List Products

http
GET /api/v1/admin/products

Query Parameters:

ParameterTypeDescription
pageintPage number (default: 1)
limitintItems per page (default: 25, max: 100)
sortstringSort field (e.g. created_at, price_gross)
orderstringasc or desc (default: desc)
searchstringFull-text search across product names
category_idUUIDFilter by category
filter[active]boolFilter by active status (true or false)

Response:

json
{
  "data": {
    "items": [
      {
        "id": "uuid",
        "sku": "TSHIRT-BLK-M",
        "active": true,
        "price_net": 1680,
        "price_gross": 1999,
        "currency": "EUR",
        "tax_rule_id": "uuid",
        "stock": 42,
        "weight": 200,
        "has_variants": true,
        "custom_fields": {},
        "metadata": {},
        "created_at": "2026-03-15T10:00:00Z",
        "updated_at": "2026-03-15T10:00:00Z",
        "translations": [
          {
            "locale": "en",
            "name": "Black T-Shirt",
            "description": "A classic black t-shirt.",
            "slug": "black-t-shirt",
            "meta_title": "Black T-Shirt",
            "meta_description": "A classic black t-shirt."
          }
        ],
        "categories": ["uuid"],
        "tags": ["uuid"],
        "media": [
          { "media_id": "uuid", "position": 0, "url": "/media/tshirt.jpg" }
        ],
        "variants": []
      }
    ]
  },
  "meta": { "total": 120, "page": 1, "limit": 25, "pages": 5 }
}

Prices

All prices are integers in the smallest currency unit (cents). 1999 = €19.99. Tax rates are in basis points: 1900 = 19.00%.

Get Product

http
GET /api/v1/admin/products/:id

Returns the full product including translations, categories, tags, media, and variants with their options.

Create Product

http
POST /api/v1/admin/products

Request Body:

json
{
  "sku": "TSHIRT-BLK-M",
  "active": true,
  "price_net": 1680,
  "price_gross": 1999,
  "currency": "EUR",
  "tax_rule_id": "uuid",
  "stock": 100,
  "weight": 200,
  "custom_fields": {},
  "metadata": {},
  "translations": [
    {
      "locale": "en",
      "name": "Black T-Shirt",
      "description": "A classic black t-shirt.",
      "slug": "black-t-shirt",
      "meta_title": "Black T-Shirt",
      "meta_description": "A classic black t-shirt for everyday wear."
    }
  ],
  "category_ids": ["uuid"],
  "tag_ids": ["uuid"]
}
FieldTypeRequiredDescription
skustringNoStock keeping unit (max 100 chars)
activeboolNoWhether the product is visible (default: false)
price_netintNoNet price in cents
price_grossintNoGross price in cents
currencystringYesISO 4217 currency code (3 chars, e.g. EUR)
tax_rule_idUUIDNoTax rule to apply
stockintNoAvailable stock (default: 0)
weightintNoWeight in grams
custom_fieldsobjectNoUser-facing custom data (JSONB)
metadataobjectNoInternal metadata (JSONB)
translationsarrayYesAt least one translation required
category_idsUUID[]NoCategories to assign
tag_idsUUID[]NoTags to assign

Translation fields:

FieldTypeRequiredDescription
localestringYesBCP 47 language tag (e.g. en, de)
namestringYesProduct name (max 255 chars)
descriptionstringNoProduct description
slugstringYesURL slug (max 255 chars)
meta_titlestringNoSEO title (max 255 chars)
meta_descriptionstringNoSEO description

Response: 201 Created with the full product object.

Update Product

http
PUT /api/v1/admin/products/:id

All fields are optional — only provided fields are updated. The request body uses the same fields as Create Product, with an additional media_ids field:

FieldTypeDescription
media_idsUUID[]Ordered list of media to attach (replaces existing)

Response: 200 OK with the updated product object.

Delete Product

http
DELETE /api/v1/admin/products/:id

Response: 204 No Content


Variants

Variants represent different configurations of a product (e.g. size, color). Each variant can override the parent product's price, SKU, and stock.

Create Variant

http
POST /api/v1/admin/products/:id/variants

Request Body:

json
{
  "sku": "TSHIRT-BLK-S",
  "price_gross": 1999,
  "price_net": 1680,
  "stock": 25,
  "active": true,
  "option_ids": ["uuid-size-s", "uuid-color-black"]
}
FieldTypeRequiredDescription
skustringNoVariant-specific SKU
price_grossintNoOverride gross price (inherits from parent if omitted or 0)
price_netintNoOverride net price (inherits from parent if omitted or 0)
stockintNoVariant stock
activeboolNoWhether variant is active
option_idsUUID[]NoProperty option IDs for this variant

Response: 201 Created

Generate Variants (Cartesian Product)

To generate all combinations from multiple option groups, use the same endpoint with option_groups instead:

http
POST /api/v1/admin/products/:id/variants
json
{
  "option_groups": [
    ["uuid-size-s", "uuid-size-m", "uuid-size-l"],
    ["uuid-color-red", "uuid-color-blue"]
  ]
}

This creates 6 variants (3 sizes × 2 colors). Each inner array represents one property axis.

Response: 201 Created with an array of all generated variants.

Update Variant

http
PUT /api/v1/admin/products/:id/variants/:variantId

Same body as Create Variant.

Response: 200 OK

Delete Variant

http
DELETE /api/v1/admin/products/:id/variants/:variantId

Response: 204 No Content


Property Groups & Options

Property groups define the axes for variants (e.g. "Size", "Color"). Options are the values within a group (e.g. "S", "M", "L").

List Property Groups

http
GET /api/v1/admin/property-groups

Returns all property groups with their options and translations. Each item includes the identifier field.

Response example:

json
{
  "data": [
    {
      "id": "uuid",
      "identifier": "shoe-size",
      "position": 0,
      "created_at": "2026-03-15T10:00:00Z",
      "updated_at": "2026-03-15T10:00:00Z",
      "translations": [
        { "locale": "en", "name": "Size" },
        { "locale": "de", "name": "Größe" }
      ],
      "options": []
    }
  ]
}

Get Property Group

http
GET /api/v1/admin/property-groups/:id

Returns a single property group including identifier, translations, and all options.

Create Property Group

http
POST /api/v1/admin/property-groups
json
{
  "identifier": "shoe-size",
  "position": 0,
  "translations": [
    { "locale": "en", "name": "Size" },
    { "locale": "de", "name": "Größe" }
  ]
}
FieldTypeRequiredDescription
identifierstringYesUnique slug: lowercase alphanumeric, hyphens, underscores (e.g. color, shoe-size). Pattern: ^[a-z0-9][a-z0-9_-]*$
positionintNoSort order
translationsarrayYesAt least one translation required

Response: 201 Created with the full property group object, including identifier.

Error responses:

StatusError codeCondition
409 Conflictduplicate_identifierAnother property group already uses this identifier
422 Unprocessable Entityinvalid_identifierIdentifier does not match the required pattern

Update Property Group

http
PUT /api/v1/admin/property-groups/:id

Same body as Create Property Group. All fields including identifier are required. Response: 200 OK

Delete Property Group

http
DELETE /api/v1/admin/property-groups/:id

Response: 204 No Content

Create Property Option

http
POST /api/v1/admin/property-groups/:id/options
json
{
  "position": 0,
  "color_hex": "#000000",
  "translations": [
    { "locale": "en", "name": "Black" },
    { "locale": "de", "name": "Schwarz" }
  ]
}
FieldTypeDescription
positionintSort order
color_hexstringOptional hex color code for visual display
translationsarrayAt least one translation required

Response: 201 Created

Update Property Option

http
PUT /api/v1/admin/property-groups/:id/options/:optId

Delete Property Option

http
DELETE /api/v1/admin/property-groups/:id/options/:optId

Bulk Import

JSON Bulk Create

http
POST /api/v1/admin/products/bulk

Creates up to 250 products in a single request. Each product can include inline variants.

json
{
  "products": [
    {
      "sku": "HOODIE-GRY",
      "active": true,
      "price_net": 3361,
      "price_gross": 3999,
      "currency": "EUR",
      "translations": [
        { "locale": "en", "name": "Grey Hoodie", "slug": "grey-hoodie" }
      ],
      "variants": [
        {
          "sku": "HOODIE-GRY-S",
          "active": true,
          "stock": 50,
          "price_net": 3361,
          "price_gross": 3999,
          "options": [
            { "group_name": "Size", "option_name": "S", "locale": "en" }
          ]
        }
      ]
    }
  ]
}

Variant options are resolved by name — property groups and options are created automatically if they don't exist.

Response: 207 Multi-Status

json
{
  "data": {
    "results": [
      { "index": 0, "sku": "HOODIE-GRY", "success": true, "id": "uuid" }
    ],
    "total": 1,
    "succeeded": 1,
    "failed": 0
  }
}

CSV Import

http
POST /api/v1/admin/products/import

Upload a CSV file as multipart/form-data (field name: file, max 10 MB).

Download the CSV template first:

http
GET /api/v1/admin/products/import/template

Response: 207 Multi-Status with the same bulk result format.


Store API

Store endpoints return only active products. No authentication required.

List Products

http
GET /api/v1/store/products

Same query parameters as admin list, except filter[active] is not available — only active products are returned.

Response: Same format as admin list.

Recursive Category Filtering

When category_id is provided, the filter uses a recursive SQL query that includes products from the given category and all of its descendant subcategories at every depth.

Electronics (id: uuid-electronics)
├── Laptops (id: uuid-laptops)
│   └── Gaming Laptops (id: uuid-gaming)
└── Phones (id: uuid-phones)
http
GET /api/v1/store/products?category_id=uuid-electronics

This returns products assigned to Electronics, Laptops, Gaming Laptops, and Phones — the entire subtree. Filtering by uuid-laptops returns products in Laptops and Gaming Laptops only.

TIP

This behaviour is identical for the admin list endpoint (GET /api/v1/admin/products?category_id=...) and the MCP tools store_list_products and admin_list_products.

Get Product by Slug

http
GET /api/v1/store/products/:slug

Looks up a product by its translated slug. The slug is matched against the locale from the Accept-Language header (defaults to en).

bash
curl http://localhost:8080/api/v1/store/products/black-t-shirt \
  -H 'Accept-Language: en'

Get Product by ID

http
GET /api/v1/store/products/id/:id

Alternative lookup by UUID. Useful when the storefront already has the product ID (e.g. from a cart).

Released under the APACHE 2.0 License.