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 Maat or Kleur, with an ordered list of values.
  • A variant is one purchasable combination of option values (for example Maat: L / Kleur: Geel). 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 /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 /api/public/v1/products/{uuid}/options

Requires products:write.

{
"options": [
{ "name": "Maat", "values": ["L", "XL"] },
{ "name": "Kleur", "values": ["Geel", "Rood"] }
]
}

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 /api/public/v1/products/{uuid}/variants

Requires products:write.

{
"variants": [
{
"sku": "KS-L-GE",
"ean": "8710000000001",
"priceCents": 4495,
"stock": 6,
"mediaUuid": "0198f3a2-…",
"optionValues": { "Maat": "L", "Kleur": "Geel" },
"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 /api/public/v1/products/{uuid}/options
GET /api/public/v1/products/{uuid}/variants

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

Delete one variant

DELETE /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 /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.

Update a variant's metafields

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

PUT /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 /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. { "Maat": "L", "Kleur": "Rood" }.
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.