Skip to main content

Product options & variants

A SitePack product can be sold as a single product or as a product with options and variants. This is the recommended way to model things like a shirt that comes in several sizes and colours: one product owns its options and variants, instead of many separate products linked together.

  • An option is an attribute such as Size or Colour, with an ordered list of values.
  • A variant is one purchasable combination of option values (for example Size: L / Colour: Yellow). Each variant has its own sku, ean, optional price override, stock, a single image, and its own properties (facets) and metafields.

All endpoints live under the versioned public API at https://api.sitepack.eu/api/public/v1 and are authorized with scopes, the same as the rest of the app API. Reading options and variants needs products:read; creating and changing them needs products:write. In SitePack core each action is guarded with the scope attribute, for example:

#[RequiredScope(PublicApiScopeEnum::PRODUCTS_WRITE)]

Set the options​

PUT https://api.sitepack.eu/api/public/v1/products/{uuid}/options

Requires products:write.

{
"options": [
{ "name": "Size", "values": ["L", "XL"] },
{ "name": "Colour", "values": ["Yellow", "Red"] }
]
}

Replaces the full set of options for the product; options that are not included are removed. Changing options does not touch the variants; upsert those separately. The response returns each stored option with its uuid, name, position and values.

Upsert the variants​

POST https://api.sitepack.eu/api/public/v1/products/{uuid}/variants

Requires products:write.

{
"variants": [
{
"sku": "KS-L-GE",
"ean": "8710000000001",
"priceCents": 4495,
"stock": 6,
"mediaUuid": "0198f3a2-…",
"optionValues": { "Size": "L", "Colour": "Yellow" },
"properties": [{ "name": "Materiaal", "value": "Katoen" }],
"metafields": [{ "name": "GTIN", "type": "text", "value": "…" }]
}
]
}

The body is the full set of variants for the product; variants that are not included are removed. Pass a variant's uuid to update it in place, or omit it to create a new one.

Field reference:

FieldDescription
skuStock keeping unit for this variant.
eanBarcode / GTIN for this variant.
priceCentsOptional price override in cents. Omit to inherit the product price.
stockAvailable stock for this variant. 0 renders as sold out on the storefront.
mediaUuidThe single image (media uuid) shown when this variant is selected.
optionValuesMap of option name → value that defines this variant.
propertiesPer-variant facets ({name, value}); they also register as site facets.
metafieldsPer-variant metafields ({name, type, value}).

Read them back​

GET https://api.sitepack.eu/api/public/v1/products/{uuid}/options
GET https://api.sitepack.eu/api/public/v1/products/{uuid}/variants

Both require products:read (an app holding products:write already has it).

Delete one variant​

DELETE https://api.sitepack.eu/api/public/v1/products/{uuid}/variants/{variantUuid}

Requires products:write.

Update a variant's stock​

Stock sits behind its own scope, the same as product-level stock: managing the catalogue must not imply moving stock. To change just one variant's stock without re-sending the whole variant set, use the dedicated endpoint:

PATCH https://api.sitepack.eu/api/public/v1/products/{uuid}/variants/{variantUuid}/stock

Requires inventory:write.

{ "stock": 12 }

stock is an absolute value and is clamped at zero. Per-location stock is managed through the full variant upsert; this endpoint sets the flat level a plain variant uses. The response returns the updated variant.

Stock history​

Every stock change is recorded, for products and for variants, whoever makes it: an order being placed, a cancelled order putting stock back, the merchant in the admin, an app through the API, MCP or an import.

The stock endpoint returns the current level with the three latest changes, newest first, and the total number of changes:

GET /api/public/v1/products/{uuid}/stock

Requires inventory:read.

{
"uuid": "0192c5d4-8f2e-7a1b-9c3d-5e6f7a8b9c0d",
"stock": 7,
"transactions": [
{
"uuid": "0192c5d8-1b2c-7d3e-8f4a-5b6c7d8e9f0a",
"delta": -1,
"from": 8,
"to": 7,
"source": "order",
"variant_uuid": null,
"order_uuid": "0192c5d9-4e7a-7b21-9f3c-2d8e6a1b5c47",
"created": "2026-06-11T09:31:04+00:00"
}
],
"transactions_total": 14
}

Page through the full history, newest first, with:

GET /api/public/v1/products/{uuid}/stock/transactions?page=1&limit=25

Requires inventory:read. limit defaults to 25 and is capped at 100. The response has the usual items and pagination (page, limit, totalRecords, totalPages) of every list endpoint.

  • delta is always to - from: positive when stock was added, negative when it was taken.
  • source is one of order, order_cancelled, admin, api, mcp, import or system.
  • variant_uuid is set when the change was to one variant's stock rather than the product's own.
  • A write that leaves the level unchanged is not recorded.

Update a variant's metafields​

To replace just one variant's metafields without touching its stock, properties or facets:

PUT https://api.sitepack.eu/api/public/v1/products/{uuid}/variants/{variantUuid}/metafields

Requires products:write.

{
"metafields": [
{ "name": "HS code", "type": "text", "value": "6109" }
]
}

The body is the full set of metafields for the variant; metafields that are not included are dropped.

Webhooks​

Changes fire thin webhooks: product.option_updated, product.variant_created, product.variant_updated and product.variant_deleted, each carrying the product uuid so you can read the current option set back.

On the storefront​

The same option set is exposed on the storefront product JSON as optionSet and through the Ecommerce SDK (getVariants, resolveVariant, and addToCart(productUuid, quantity, variantUuid)).

On orders​

When a shopper buys a variant, the order remembers exactly which one. Reading an order back with the orders:read scope returns each line with the ordered variant, so fulfilment and ERP integrations always know what was bought:

GET https://api.sitepack.eu/api/public/v1/orders/{uuid}

Each entry in the order's items carries, for a variant line:

FieldDescription
is_varianttrue when the line is an option variant, false for a plain product.
variant_uuidThe uuid of the ordered variant.
variation_combinationThe chosen option set, e.g. { "Size": "L", "Colour": "Red" }.
variation_skuThe ordered variant's SKU.
product_barcodeThe ordered variant's EAN as recorded on the line.

A plain (non-variant) line reports is_variant: false with variant_uuid and variation_combination set to null.

Order a variant through the API​

An app that creates orders (POST /api/public/v1/orders, scope orders:write) orders a product with option variants per variant: set variant_uuid on the line item to the uuid of the size or colour to send, as returned by GET /api/public/v1/products/{uuid}/variants. The variant's price and stock then apply. A plain product needs no variant_uuid.

POST https://api.sitepack.eu/api/public/v1/orders
{
"items": [
{ "product_uuid": "0192c50a-8d21-7b45-9e6f-0c73a2915d8b", "quantity": 1 },
{
"product_uuid": "0192c50a-8d21-7b45-9e6f-0c73a2915d8b",
"variant_uuid": "0192c60a-3b18-7e29-9d75-05af7c831e42",
"quantity": 2
}
],
"customer": {
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Doe",
"street": "Main Street",
"number": "12",
"postal_code": "1012 AB",
"city": "Amsterdam",
"country_code": "NL"
}
}
  • A line for a product that has option variants but no variant_uuid is rejected with 422 (field: items): the order cannot say which variant to send or which stock to take.
  • A variant_uuid that does not belong to the line's product_uuid returns 404.
  • Prices always come from the catalogue; a price sent in the body is ignored.
  • payment_method is encoded as gateway__method, the method being a number. Left out, it defaults to api__98 (manual invoice): the order is created unpaid and taking the money stays a PSP concern. A value in any other shape is rejected with 422.

Stock is taken as soon as the order is placed: from the variant on a variant line, from the product on a plain line. A variant that keeps stock per stock location has it taken from the location holding the most first, spilling over to the next; when a cancelled order puts the stock back, it goes to the location holding the most. The product's own stock of a product with variants follows the total of its variants. Each of these moves is recorded in the stock history with source: "order" (or order_cancelled), the order_uuid and, for a variant line, its variant_uuid, so you can read them back with GET /api/public/v1/products/{uuid}/stock/transactions.