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
MaatorKleur, 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 ownsku,ean, optional price override,stock, a single image, and its ownproperties(facets) andmetafields.
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:
| Field | Description |
|---|---|
sku | Stock keeping unit for this variant. |
ean | Barcode / GTIN for this variant. |
priceCents | Optional price override in cents. Omit to inherit the product price. |
stock | Available stock for this variant. 0 renders as sold out on the storefront. |
mediaUuid | The single image (media uuid) shown when this variant is selected. |
optionValues | Map of option name → value that defines this variant. |
properties | Per-variant facets ({name, value}); they also register as site facets. |
metafields | Per-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:
| Field | Description |
|---|---|
is_variant | true when the line is an option variant, false for a plain product. |
variant_uuid | The uuid of the ordered variant. |
variation_combination | The chosen option set, e.g. { "Maat": "L", "Kleur": "Rood" }. |
variation_sku | The ordered variant's SKU. |
product_barcode | The 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.