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.