Skip to content

Variant Groups

Overview

A Variant Group bundles several existing menu items that represent variations of the same product (for example the small, medium and large versions of a pizza) into a single selectable product. The group itself carries a shared name, PLU and description, plus optional shared modifier groups; pricing, SKU/PLU and inventory stay on the individual items.

Key Purpose: Group existing items as selectable variations of one product so customers pick a variant (e.g. a size) instead of seeing separate items.

Availability: In production, variant groups are only exposed to merchants with a Square integration (is_square_integrated). A non-Square production merchant sees no "New variant group" option on the Items list and no "Variant Group" type filter — outside Square, the feature is limited to the test and staging environments.

Purpose

This page lets you create and manage variant groups. You select existing items to act as the variants, give the group a name, PLU and description, order the variants, and optionally attach modifier groups that apply across all variants in the group.

Key Concepts

  • Variant Group: A named grouping of existing items, with shared name, plu, description, an items_order map, and optional shared modifier_group_ids.
  • Items as Variants: Each variant is a full existing item with its own price, PLU and inventory. An item is linked to a group through its own variant_group_id field; the group has no separate "option" records of its own (except for Square-managed groups, which mirror Square variation IDs in option_ids).
  • Label Consistency: All items in a group must share the same label (the integration/source label such as Regular or Square), and any attached modifier groups must share that label too; mismatches are rejected.
  • Tax Category Consistency: All items in a group must have the same tax rate code; mixed tax categories are rejected.
  • Items Order: The group stores an items_order map ({ item_id: position }) that controls the display sequence of the variants. The order is taken from the position of items in the submitted items array (drag-and-drop reordering in the UI).

Actions

Create Variant Group

Provide a name, PLU (auto-generated from the name if left blank) and optional description, then add a list of existing items to bundle as variants. Modifier groups can optionally be attached. When Square is integrated, the old standalone Square catalog objects for the items being added are cleaned up first to avoid ID conflicts when Square creates the variation entries.

Edit Variant Group

Update the group's name, PLU, description, item list or attached modifier groups. Items removed from the group have their variant_group_id cleared so they revert to standalone items; newly added items are validated and linked.

Delete Variant Group

Remove the variant group. Every item that belonged to the group has its variant_group_id cleared, so the items are preserved as standalone items — no items are deleted.

Location

  • Backoffice create page: /menus/items/variant-groups/new
  • Backoffice edit page: /menus/items/variant-groups/edit/:id
  • Navigation: Reached from the Items list (/menus/items) via the "Add" menu. Variant groups appear in the same list as items. In production the "New variant group" entry and the "Variant Group" type filter are shown only to Square-integrated merchants, and the entry is additionally hidden when the location's connected POS owns the catalog read-only (see Business Rules).
  • Backend API base: /back-office/variant-groups
  • Backend Controller: app/Http/Controllers/Api/BackOffice/VariantGroupController.php
  • Backend Model: app/RawModels/VariantGroup.php (Mongo menus collection, model: variant_group)
  • Vue Form: src/components/forms/variant-group/VariantGroupForm.vue (sub-views under src/views/variant-group/)
  • Vuex module: src/store/modules/variantGroup.ts

API Endpoints

All endpoints are under the back-office API prefix /back-office/variant-groups.

MethodPathActionPermission
POST/back-office/variant-groupsCreate a variant groupcreate-variant-groups
GET/back-office/variant-groups/{id}Show a variant groupview-items
PUT/back-office/variant-groups/{id}Update a variant groupedit-variant-groups
DELETE/back-office/variant-groups/{id}Delete a variant groupdelete-variant-groups

Variant Group Fields

These are the fields accepted by the create/update request (StoreVariantGroupRequest).

Name

PropertyValue
Field IDname
LabelName
TypeText
RequiredYes
Validationrequired, string

Description: Name of the variant group. Stored in details.default.name. Used to auto-generate the PLU when no PLU is supplied.

Examples:

  • "Margherita Pizza"
  • "House Latte"

PLU

PropertyValue
Field IDplu
LabelPLU
TypeText
RequiredYes
Validationrequired, string, unique per location

Description: Product Look-Up code for the group. If omitted, it is auto-generated from the name. Must be unique within the location. For Square-sourced groups the PLU is taken from the first item and is not editable.


Description

PropertyValue
Field IDdescription
LabelDescription
TypeTextarea
RequiredNo
Validationnullable, string (UI limits to 300 characters)

Description: Optional description of the variant group. Stored in details.default.description.


Category

PropertyValue
Field IDcategory_id
LabelCategory
TypeSelect
RequiredNo
Validationnullable; the category must exist for this merchant

Description: Optional category for the variant group, mirroring the same field on an item. Leaving it empty is valid — a group does not have to be categorised.


Items (Variations)

PropertyValue
Field IDitems
LabelVariations
TypeArray of { id }
RequiredYes
Validationrequired, array; each item must exist; each entry needs items.*.id

Description: The existing items to bundle as variants. The submitted order becomes the items_order map. Each item's variant_group_id is set to this group. All items must share the same label and the same tax category.


Modifier Groups

PropertyValue
Field IDmodifier_groups
LabelModifier Groups
TypeArray of { id }
RequiredNo
Validationnullable, array; each modifier group must exist; each entry needs modifier_groups.*.id

Description: Optional modifier groups attached at the group level so they apply across all variants. Attached modifier groups must share the items' label.


Status

PropertyValue
Field IDstatus
LabelStatus
TypeString (API only — not exposed on the back-office form)
RequiredNo
Validationnullable, string, in: Active, Inactive

Description: Active/Inactive status of the group. Defaults to Active when not set. The back-office create/edit form never sends status, so groups created there are always Active; there is no Activate/Deactivate control for variant groups in the UI.


Location

PropertyValue
Field IDlocation_id
LabelLocation
TypeString (location id)
RequiredYes (on create only)
Validationrequired on create, string, must exist in locations

Description: The location the variant group belongs to. Required when creating; not required (and not changed) on update.


Variants vs Modifiers

FeatureVariant GroupModifiers
What it isA group of existing itemsAdd-on choices on an item
PricingEach variant item keeps its own priceAdd-on price per modifier
PLU / inventoryPer variant itemOn the base item
ExampleSmall / Medium / Large pizzaExtra cheese, no onions
SelectionCustomer picks one variantCustomer picks add-ons

When to use a Variant Group:

  • Different versions of a product that already exist as separate items (sizes, flavours)
  • Each version has its own price, PLU and inventory
  • You want them to appear as one selectable product instead of separate items

When to use Modifiers:

  • Optional add-ons or customisations on a single item
  • Shared with the base item rather than separate products

Business Logic

Pricing

The variant group does not store a price. Pricing comes from the individual items in the group. The back office shows a price range across the group's items (price_range with min/max); when both ends match it shows a single price. Modifier prices on the items widen that range.

How variants attach to items

There is no per-option record holding a price adjustment or SKU. Attachment is one-directional:

  • The item carries the link in its own variant_group_id field.
  • The group carries the ordering in its items_order map and (for Square only) the Square variation IDs in option_ids.
  • On save, each submitted item's variant_group_id is set to the group id, and the group's items_order is rebuilt from the submitted order.

Square integration

When Square is integrated, creating a group (or adding items to one) first deletes the old standalone Square catalog objects for those items and clears their Square IDs, so Square can recreate them as variations under the group. Deleting a group syncs the deletion to Square; for non-Square setups, deletion simply clears variant_group_id on the affected items.


Customer Impact

Online Ordering and Kiosk

A variant group is serialized to the storefront/kiosk as a single product whose variants are the grouped items, together with the group's shared modifier_groups, image (taken from the first matching item) and a base price range. Customers pick one of the variants.

Exact storefront/kiosk rendering of the variant selector is not verified here (the back-end only provides the serialized variants, modifier_groups and price range).


Relations

Depends On

  • Menu Items: A variant group is built from existing items, each linked via variant_group_id.

Affects

  • Online Ordering: Items in a group are presented as variants of one product.
  • Kiosk: Same grouping is used for the kiosk menu.
  • Modifier Groups: Optional shared modifier groups can be attached to the group.
  • Square / third-party integrations: Creates/updates/deletes are synced to active integrations.

Business Rules

  • Creating a variant group is blocked with HTTP 422 when the location's connected POS owns the catalog read-only — currently Hendrickx and Vanhoutte (both Kassanet/Vectron). The error reads "This action isn't available while {POS} is your connected POS — its catalog is managed in {POS}." The back office mirrors this by hiding the "New variant group" option for those merchants.
  • All items in a variant group must have the same tax rate code; grouping items with different tax categories returns a 409 error ("All items must have same tax category.").
  • All items in a variant group must share the same label; mismatched labels return a 400 error ("All items must have the same label").
  • Any attached modifier groups must share the items' label; a mismatch returns a 400 error ("All modifier groups must have the same label").
  • An item can only belong to one variant group at a time; adding an item that already belongs to another group returns a 400 error ("Item …: is already in another variant group").
  • The PLU must be unique per location; if no PLU is provided it is auto-generated from the name.
  • When items are removed from a group during an update, their variant_group_id is cleared so they revert to standalone items.
  • Deleting a group preserves its items as standalone items (their variant_group_id is cleared); no items are deleted.
  • For Square-sourced groups, the PLU, item list and modifier groups are uneditable (uneditable_fields).
  • Creating a group (or adding items) cleans up the old standalone Square catalog objects for those items to prevent ID conflicts when Square creates variation entries.

FAQs

  • What is the difference between a variant group and modifiers? A variant group bundles existing items (e.g. Small vs Large pizza), each keeping its own price, PLU and inventory. Modifiers are add-on customisations on a single item (e.g. extra cheese).
  • Do I create new variants here, or pick existing items? You pick existing items. A variant group is built from items that already exist in your menu; it does not create new items with their own price-adjustment fields.
  • Can I attach modifier groups to a variant group? Yes. Modifier groups attached at the group level apply across all variants. They must have the same label as the items.
  • What happens when I delete a variant group? The group is removed, but every item in it is preserved as a standalone item with its variant_group_id cleared. No items are deleted.
  • Can the variants have different prices? Yes. Each item keeps its own price; the group itself stores no price and shows a price range across its items.
  • How is the display order of variants determined? The group stores an items_order map built from the order of items in the submitted array; the back office lets you drag to reorder.
  • Why can't I edit the PLU or items on some groups? Groups sourced from Square have their PLU, items and modifier groups locked (uneditable_fields) because Square owns that data.

Troubleshooting

Problem: "This action isn't available while {POS} is your connected POS — its catalog is managed in {POS}." (HTTP 422)

Cause: The location is connected to a POS whose catalog is read-only inbound (Hendrickx or Vanhoutte, both Kassanet/Vectron). A variant group created in Upvendo could not be pushed back to that POS.

Solution: Create the variation structure in the POS instead; it syncs into Upvendo. The "New variant group" option is hidden in the back office for these merchants, so this error normally only appears when calling the API directly.


Problem: I can't find "New variant group" on the Items list

Cause: In production the option is only shown to Square-integrated merchants, and it is hidden when the connected POS owns the catalog read-only.

Solution: Nothing to fix — the feature is not offered for that merchant in production.


Problem: "All items must have same tax category."

Cause: The selected items have different tax rate codes.

Solution: Only group items that share the same tax category.


Problem: "All items must have the same label" / "All modifier groups must have the same label"

Cause: The selected items (or attached modifier groups) come from different sources/labels.

Solution: Group only items that share the same label, and attach only modifier groups with the same label.


Problem: "Item …: is already in another variant group"

Cause: One of the selected items already belongs to a different variant group.

Solution: Remove the item from its other group first, or choose a different item.


Examples

Create request (POST /back-office/variant-groups)

json
{
  "name": "Margherita Pizza",
  "plu": "MAR250616120000",
  "description": "Choose your size",
  "location_id": "664b1f...",
  "items": [
    { "id": "664c01..." },
    { "id": "664c02..." },
    { "id": "664c03..." }
  ],
  "modifier_groups": [
    { "id": "665a10..." }
  ]
}

Update request (PUT /back-office/variant-groups/{id})

json
{
  "name": "Margherita Pizza",
  "plu": "MAR250616120000",
  "description": "Choose your size",
  "items": [
    { "id": "664c02..." },
    { "id": "664c01..." }
  ],
  "modifier_groups": []
}

Reordering the items array changes the saved items_order. Dropping 664c03... from the list clears that item's variant_group_id, restoring it as a standalone item.

Show response (GET /back-office/variant-groups/{id})

json
{
  "id": "665d77...",
  "location_id": "664b1f...",
  "name": "Margherita Pizza",
  "plu": "MAR250616120000",
  "description": "Choose your size",
  "items": [
    {
      "id": "664c01...",
      "name": "Margherita Pizza (Small)",
      "image_url": "https://...",
      "modifier_groups": [],
      "price_range": { "min": 10, "max": 10 }
    }
  ],
  "modifier_groups": [
    {
      "id": "665a10...",
      "name": "Extras",
      "modifiers": [ { "id": "665a11...", "name": "Extra cheese" } ]
    }
  ],
  "uneditable_fields": []
}