Moloni ON Logo WhiteGuidesAPI ReferenceExplorer
Guides

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:

  1. Create a property group with properties (Color, Size) and values (Red, Blue, S, M, L)
  2. Create a parent product with propertyGroupId set
  3. Add each variant with productVariantCreate, giving it the propertyPairs that identify it, one value per property
  4. The API generates each variant's reference from 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

ConditionMessage
propertyPairs is emptyAt least a propertyPair is required
The parent has no property groupThis product doesn't have a property group, so it can't have variants.
productId points at a variantCan't create a variant of a variant.
Another variant already uses that combination of valuesA variant with that combination of property values already exists
The parent has hasStock: true and no warehouses were sentVariant must have warehouses when parent hasStock is enabled
The parent has hasStock: false and warehouses were sentCan't send warehouses because the parent product doesn't have stock enabled
The resulting reference is already takenA 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 Service
  • companyId
  • measurementUnitId
  • productCategoryId
  • hasStock
  • exemptionReason
  • posFavorite
  • notesOnExport
  • taxes
  • suppliers
  • customFields
  • 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:

FieldDescription
nameVariant display name
referenceFull product reference (generated when omitted)
pricePrice (can differ from parent)
summaryDescription
notesInternal notes
visibleVisibility flag
imgVariant-specific image
minStockMinimum stock threshold
warehousesStock per warehouse
priceClassesPrice class overrides
identificationsBarcodes specific to this variant

Variant input fields

The ProductVariantCreate input:

FieldTypeDescription
nameString!Variant name (required)
referenceStringFull reference. Omit it (or send an empty string) to have it generated
priceFloatVariant price
summaryStringDescription
notesStringInternal notes
visibleIntVisibility flag
imgUploadImage
minStockFloatMinimum stock threshold
warehouseIdIntDefault 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 ReferenceColor CodeSize CodeVariant Reference
TSHIRTREDSTSHIRT-RED-S
TSHIRTBLUEXLTSHIRT-BLUE-XL
MUG-CERAMICWHITE(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

© 2026 Moloni ON

Tax Authority Certificate No. 3075