Variants
Variants represent different versions of a product, for example a T-Shirt in multiple colors and sizes. Each variant is a separate product linked to a parent, with its own price, stock and identifiers.
How variants work
Variants build on property groups:
- Create a property group with properties (Color, Size) and values (Red, Blue, S, M, L)
- Create a parent product with
propertyGroupIdset - Add each variant with
productVariantCreate, giving it thepropertyPairsthat identify it, one value per property - The API generates each variant's
referencefrom the parent reference plus the value codes, unless you send your own
Parent: "TSHIRT" (propertyGroup: "T-Shirt Options")
├── Variant: "TSHIRT-RED-S" (Color=Red, Size=Small) → price: 15.99
├── Variant: "TSHIRT-RED-M" (Color=Red, Size=Medium) → price: 15.99
├── Variant: "TSHIRT-BLUE-S" (Color=Blue, Size=Small) → price: 16.99
└── Variant: "TSHIRT-BLUE-M" (Color=Blue, Size=Medium) → price: 16.99
A variant is an ordinary product that carries a parentId, so only creation has a dedicated mutation. Once a variant exists you edit it with productUpdate and remove it with productDelete, addressing it by its own productId just like any other product.
Creating the parent product
Set propertyGroupId on the parent. You need the UUIDs from your property group, so if you haven't created one yet see the Property Groups guide.
mutation {
productCreate(
companyId: 1
data: {
name: "Classic T-Shirt"
reference: "TSHIRT"
type: 1
productCategoryId: 5
measurementUnitId: 1
price: 15.99
hasStock: true
propertyGroupId: "group-uuid"
taxes: [
{ taxId: 1, value: 23, ordering: 1, cumulative: false }
]
}
) {
errors { field msg }
data {
productId
name
reference
variantsCount
}
}
}
The parent starts with variantsCount: 0. Don't plan on setting the parent's own stock: once it has variants, its stock per warehouse tracks the total of its variants' stock, which the API maintains as you add them.
Adding variants
productVariantCreate adds one variant to an existing parent without resending its siblings:
mutation {
productVariantCreate(
companyId: 1
productId: 100
data: {
name: "Red Small"
price: 15.99
propertyPairs: [
{ propertyId: "color-uuid", propertyValueId: "red-uuid" }
{ propertyId: "size-uuid", propertyValueId: "small-uuid" }
]
warehouses: [
{ warehouseId: 1, stock: 25 }
]
}
) {
errors { field msg }
data {
productId
parentId
name
reference
price
propertyPairs {
property { name }
propertyValue { value code }
ordering
}
warehouses {
warehouseId
stock
}
}
}
}
Response:
{
"data": {
"productVariantCreate": {
"errors": [],
"data": {
"productId": 101,
"parentId": 100,
"name": "Red Small",
"reference": "TSHIRT-RED-S",
"price": 15.99,
"propertyPairs": [
{ "property": { "name": "Color" }, "propertyValue": { "value": "Red", "code": "RED" }, "ordering": 1 },
{ "property": { "name": "Size" }, "propertyValue": { "value": "Small", "code": "S" }, "ordering": 2 }
],
"warehouses": [{ "warehouseId": 1, "stock": 25 }]
}
}
}
}
Send one call per combination. Each call is independent, so a rejected variant leaves the ones you already created untouched.
Rules the API enforces
| Condition | Message |
|---|---|
propertyPairs is empty | At least a propertyPair is required |
| The parent has no property group | This product doesn't have a property group, so it can't have variants. |
productId points at a variant | Can't create a variant of a variant. |
| Another variant already uses that combination of values | A variant with that combination of property values already exists |
The parent has hasStock: true and no warehouses were sent | Variant must have warehouses when parent hasStock is enabled |
The parent has hasStock: false and warehouses were sent | Can't send warehouses because the parent product doesn't have stock enabled |
| The resulting reference is already taken | A product with that reference already exists |
The values in propertyPairs must belong to the parent's property group. A value from another group is rejected.
What variants inherit from the parent
When created, variants automatically inherit these fields from the parent product:
type: Product or ServicecompanyIdmeasurementUnitIdproductCategoryIdhasStockexemptionReasonposFavoritenotesOnExporttaxessupplierscustomFields- the AT product type
minStock also falls back to the parent's value when you omit it.
What variants can override
Each variant can have its own:
| Field | Description |
|---|---|
name | Variant display name |
reference | Full product reference (generated when omitted) |
price | Price (can differ from parent) |
summary | Description |
notes | Internal notes |
visible | Visibility flag |
img | Variant-specific image |
minStock | Minimum stock threshold |
warehouses | Stock per warehouse |
priceClasses | Price class overrides |
identifications | Barcodes specific to this variant |
Variant input fields
The ProductVariantCreate input:
| Field | Type | Description |
|---|---|---|
name | String! | Variant name (required) |
reference | String | Full reference. Omit it (or send an empty string) to have it generated |
price | Float | Variant price |
summary | String | Description |
notes | String | Internal notes |
visible | Int | Visibility flag |
img | Upload | Image |
minStock | Float | Minimum stock threshold |
warehouseId | Int | Default warehouse |
propertyPairs | [ProductVariantPropertyPairsAssociation!] | Property value assignments |
warehouses | [ProductWarehouseInsertAssociation!] | Stock per warehouse |
priceClasses | [ProductPriceClassAssociation!] | Price class values |
identifications | [ProductIdentification!] | Barcodes |
Stock requirements
If the parent has hasStock: true, every variant must include at least one warehouses entry. If the parent has hasStock: false, variants cannot include warehouses.
Querying a product with its variants
query {
product(companyId: 1, productId: 100) {
data {
productId
name
reference
price
variantsCount
propertyGroup {
propertyGroupId
name
properties {
propertyId
name
ordering
values {
propertyValueId
code
value
}
}
}
variants {
productId
name
reference
price
visible
stock
propertyPairs {
property { name ordering }
propertyValue { value code }
ordering
}
warehouses {
warehouseId
stock
minStock
}
}
}
}
}
The variants field accepts an optional visible parameter to filter by visibility:
variants(visible: 1) {
productId
name
price
}
Filtering variants in product lists
When listing products with the products query, variants appear as separate products with a non-null parentId. You can use this to distinguish parent products from variants:
query {
products(companyId: 1, options: {
pagination: { page: 1, qty: 20 }
}) {
data {
productId
parentId
name
reference
price
variantsCount
}
}
}
parentId: null→ this is a parent product (or a standalone product)parentId: 100→ this is a variant of product 100
Updating a variant
Call productUpdate with the variant's own productId. Only the fields you send are updated, and the sibling variants are never involved:
mutation {
productUpdate(
companyId: 1
data: {
productId: 101
price: 17.99
}
) {
errors { field msg }
data {
productId
reference
price
}
}
}
The same call edits a variant's reference, name, warehouses, priceClasses and identifications.
Removing a variant
Delete it like any other product, by its own productId:
mutation {
productDelete(companyId: 1, productId: [104]) {
status
deletedCount
errors { field msg }
}
}
Deleting the parent removes its variants with it. See Deletion for how the delete mutations report skipped records.
Reference generation
When you omit reference (or send an empty string), the API builds one in this format:
{parentReference}-{code1}-{code2}-...
The codes are taken from the property values in the order defined by the property's ordering field, not the order in which you list propertyPairs. For example:
| Parent Reference | Color Code | Size Code | Variant Reference |
|---|---|---|---|
TSHIRT | RED | S | TSHIRT-RED-S |
TSHIRT | BLUE | XL | TSHIRT-BLUE-XL |
MUG-CERAMIC | WHITE | (none) | MUG-CERAMIC-WHITE |
Send reference yourself when you need a specific SKU. The value is used exactly as given, with no codes appended.
The full variant reference must fit within 50 characters (the database limit for all product references). Because of this, parent products that have variants are limited to a 30 character reference, leaving room for the separator and codes.
Keep property value codes short to avoid hitting this limit. If a reference exceeds 50 characters or conflicts with an existing product, the API returns a validation error rather than adjusting it for you. The same applies to a generated one, so two values sharing a code, or a product already holding the generated reference, means you have to supply a reference explicitly.
Legacy: the parent's variants array
Passing a variants array to productCreate creates the parent and its variants in one call:
mutation {
productCreate(
companyId: 1
data: {
name: "Classic T-Shirt"
reference: "TSHIRT"
type: 1
productCategoryId: 5
measurementUnitId: 1
price: 15.99
hasStock: true
propertyGroupId: "group-uuid"
variants: [
{
name: "Red Small"
price: 15.99
propertyPairs: [
{ propertyId: "color-uuid", propertyValueId: "red-uuid" }
{ propertyId: "size-uuid", propertyValueId: "small-uuid" }
]
warehouses: [{ warehouseId: 1, stock: 25 }]
},
{
name: "Red Medium"
price: 15.99
propertyPairs: [
{ propertyId: "color-uuid", propertyValueId: "red-uuid" }
{ propertyId: "size-uuid", propertyValueId: "medium-uuid" }
]
warehouses: [{ warehouseId: 1, stock: 40 }]
}
]
}
) {
errors { field msg }
data { productId reference variantsCount }
}
}
On productUpdate the array is a full replacement of the parent's variants:
- With
productId→ update that existing variant - Without
productId→ create a new variant - Existing variants not in the array → deleted
Any stock you declare on the parent in the same call is discarded and recomputed as the sum of the variants' stock per warehouse.
References are always generated on this path. ProductVariantInsert and ProductVariantUpdate have no reference field, so a reference you want to choose yourself has to go through productVariantCreate or a follow-up productUpdate on the variant.
Next steps
- Property Groups: Manage the dimensions that power variants
- Overview & Creating a Product: Back to product fundamentals
- Creating an Invoice: Use products and variants in documents
- See
ProductVariantCreatefor every field accepted when adding a variant