Skip to content

Modifiers & Options

Overview

Modifiers allow customers to customize menu items. They are organized into Modifier Groups (e.g., "Size", "Toppings") containing individual options (e.g., "Small", "Medium", "Large").

Key Purpose: Enable product customization with extras, sizes, or removals.

Purpose

This page lets you create and manage modifier groups and their individual options so customers can customize menu items with extras, sizes, or removals.

Key Concepts

  • Modifier Group: A named collection of related options (e.g., "Size", "Toppings") with a pricing mode, channel visibility, and settings for Optional/Required, allow-multiple-selection, allow-same-modifier-more-than-once, and a maximum number selectable.
  • Modifier (Option): An individual choice within a group with a customer-facing name, an optional kitchen_name, an absolute price (Money — not a +/- adjustment), and an optional image.
  • Pricing Mode: One of No charge (all modifiers free), Individual (each modifier has its own price, stored in a modifier_prices map on the group), or Group (all modifiers share a single group price). A tax rate can be set for Individual and Group pricing.
  • Reordering: Modifier groups have an order field used for display sequence; it is set to the current count of groups on creation. (This field is marked @deprecated in the model.)

Actions

Create Modifier Group

Add a new modifier group with a name (and optional description and POS name), pricing mode, tax rate (if not "No charge"), channel visibility, settings, and a list of modifier options. Each option is created and synced, and the group is pushed to every active third-party integration (Square, MplusKassa, …).

Creation is blocked outright when the location's connected POS owns the catalog read-only: modifier_group_create is disabled for Hendrickx, Vanhoutte and ShopCaisse, which return a 422 "This action isn't available while <POS> is your connected POS — its catalog is managed in <POS>." (Verified: upvendo-backend app/Services/BackOffice/ModifierGroupService.php lines 133-137; config/pos-providers.php lines 36-39, 60-63, 94-99; app/Helpers/ThirdPartyIntegrationHelper.php lines 68-76.)

Edit Modifier Group

Update the group's name, settings, pricing, visibility, or its list of modifier options. Existing modifiers are updated in place; new ones are created. A ReloadMenu event fires so ordering channels reflect changes immediately.

Delete Modifier Group

Remove a modifier group permanently. The deletion is pushed to every active third-party integration and triggers a menu reload event. Deleting a ShopCaisse-labelled group is blocked with the same 422 message — ShopCaisse is the only provider that sets modifier_group_delete => false. (Verified: upvendo-backend app/Services/BackOffice/ModifierGroupService.php lines 178-183; config/pos-providers.php lines 102-104.)

Reorder Modifier Groups

Change the display order of modifier groups by submitting a new ordering array. Each group's order field is updated accordingly.

Location

  • Backoffice Route: /menus/modifiers (Navigation: Menus → Modifier Groups)
  • Backend Controller: app/Http/Controllers/Api/ModifierGroupController.php
  • Vue Component: src/views/menus/modifiers/ModifierGroups.vue (mounted by src/pages/menus/modifiers/index.vue)

Concepts

Modifier Group

A collection of related options. Examples:

  • "Size" (Small, Medium, Large)
  • "Toppings" (Cheese, Bacon, Mushrooms)
  • "Sauce" (Ketchup, Mayo, BBQ)

Modifier Option

An individual choice within a group. Examples:

  • "Large" (€2.00)
  • "Extra Cheese" (€1.50)
  • "No Onions" (€0.00)

Modifier Group Fields

Group Name

PropertyValue
Field IDdetails.name
LabelModifier Group Name
TypeText
RequiredYes
Validationrequired, string

Description: Name of the modifier group, used internally and shown to customers (kiosk, online ordering, etc.) unless a POS Name overrides it.

Examples:

  • "Size"
  • "Choose Your Toppings"
  • "Select a Sauce"

Description

PropertyValue
Field IDdetails.description
LabelDescription
TypeText
RequiredNo
Validationnullable, string (UI enforces max 300 characters)

Description: Optional descriptive text for the modifier group.


POS Name

PropertyValue
Field IDpos_name
LabelPOS Name
TypeText
RequiredNo
Validationnullable, string, unique per location

Description: Optional internal name (e.g., a shorter label for the kitchen printer). Must be unique within the location. If left blank, the regular name is used everywhere.


Pricing

PropertyValue
Field IDpricing
LabelPricing
TypeEnum
RequiredYes
Allowed Valuesno-charge, individual, group

Description: How modifiers in the group are priced.

Business Logic:

  • No charge (no-charge): All modifiers are free (every modifier price is set to 0).
  • Individual (individual): Each modifier has its own price (stored in the group's modifier_prices map).
  • Group (group): All modifiers share the single group price.

Group Price

PropertyValue
Field IDprice
LabelPrice
TypeCurrency (Money)
RequiredOnly when pricing is group
Validationrequired, numeric (when pricing = group)

Description: The single shared price applied to every modifier when Group pricing is selected.


Tax Rate Code

PropertyValue
Field IDtax_rate_code
LabelTax Rate
TypeString
RequiredNo
Validationnullable, string (only accepted when pricing is not no-charge)

Description: Tax rate applied to the group's modifiers. Not required, and not applicable for No charge pricing.


Visibility (Channels)

PropertyValue
Field IDvisibility
LabelChannels
TypeArray of channel enums
RequiredYes
Allowed ValuesKiosk, POS, Online Ordering, Table Qr Ordering, Uber Eats, Takeaway, Shopify

Description: Sales channels on which the modifier group is visible. At least one channel must be selected.


Settings — Selection Type

PropertyValue
Field IDsettings.is_mandatory
LabelSelection Type (Optional / Required)
TypeBoolean
RequiredYes
Defaultfalse (Optional)

Description: When true (Required), the customer must pick a modifier from this group. When false (Optional), selection is not forced.


Settings — Allow Multiple Selection

PropertyValue
Field IDsettings.allow_select_more_than_one
LabelAllow customers to select more than one modifier
TypeBoolean
RequiredYes
Defaultfalse

Description: When true, customers can select multiple modifiers from the group, and a maximum-selectable dropdown appears.


Settings — Maximum Selected

PropertyValue
Field IDsettings.max_selected
LabelMaximum of modifiers a customer can select
Type"No Maximum" or a number (2..N)
RequiredRequired only when allow_select_more_than_one is true

Description: The maximum number of modifiers a customer can choose. Options are "No Maximum" or a specific number up to the count of modifiers in the group. Only shown/required when multiple selection is enabled.

Note on MplusKassa-synced groups: unlike the group's other settings, settings.max_selected stays editable for modifier groups synced from MplusKassa — it syncs bi-directionally back to MplusKassa (selectionRangeMax / maxOneSelection). For every other integration that locks fields (Hendrickx/Vanhoutte, ShopCaisse, Lightspeed K-Series), this field is locked along with the rest of the group's settings. See the POS field-locking FAQ below for the full breakdown.


Settings — Allow Same Modifier More Than Once

PropertyValue
Field IDsettings.allow_same_modifier_more_than_one
LabelAllow customers to select the same modifier more than once
TypeBoolean
RequiredYes
Defaultfalse

Description: When true, a customer can add the same modifier multiple times (e.g., double sugar, double sauce).


Order (deprecated)

PropertyValue
Field IDorder
Label(internal)
TypeNumber
RequiredNo (system-managed)

Description: Display-ordering value, set to the current count of groups on creation. Marked @deprecated in the model; reordering still writes to it via the reorder endpoint.


Modifier Option Fields

Option Name

PropertyValue
Field IDname
LabelName
TypeText
RequiredYes
Validationrequired, string

Description: Customer-facing name of the option.

Examples:

  • "Small"
  • "Extra Cheese"
  • "No Pickles"

Kitchen Name

PropertyValue
Field IDkitchen_name
LabelKitchen Name
TypeText
RequiredNo
Validationnullable, string

Description: Optional clearer/shorter label for kitchen and printer staff.


Price

PropertyValue
Field IDprice
LabelPrice
TypeCurrency (Money — absolute amount, not a +/- adjustment)
RequiredYes
Validationrequired, numeric

Description: The modifier's price as an absolute amount. With Group pricing every modifier is forced to the group price; with No charge every modifier is set to 0; with Individual pricing each modifier keeps its own value.

Examples:

  • "Large": 2.00
  • "Extra Cheese": 1.50
  • "Ketchup": 0.00

Image

PropertyValue
Field IDimage (image.source, image.file)
LabelImage
TypeFile upload
RequiredNo
Validationimage.source is a valid image source; image.file max 10240 KB

Description: Optional image for the modifier option, stored as a content entity (its content_id is saved on the modifier record). Used where modifier images are shown (e.g., kiosk).


Business Logic

Price Calculation with Modifiers

Item Base Price: €10.00

Selected Modifiers (absolute prices):
- Size: Large (€2.00)
- Extra Cheese (€1.50)
- Bacon (€2.00)
- No Onions (€0.00)

Item Total: €10.00 + €2.00 + €1.50 + €2.00 + €0.00 = €15.50

Quantity: 2

Line Total: €15.50 × 2 = €31.00

Pricing Modes

No charge  → every modifier price is forced to 0
Individual → each modifier keeps its own price (stored in group's modifier_prices map)
Group      → every modifier is forced to the single group price

Validation Flow

Customer selects item


For each modifier group attached to item:

        ├── Is group Required (settings.is_mandatory)?
        │   └── Yes → Must have a selection

        ├── Is multiple selection allowed (allow_select_more_than_one)?
        │   └── Yes → Enforce settings.max_selected ("No Maximum" or a number)

        └── Validate all rules pass


        Allow "Add to Cart"

Customer Impact

Online Ordering Flow

  1. Customer clicks item
  2. Item detail modal opens
  3. Modifier groups displayed in order
  4. Required groups marked with badge
  5. Customer makes selections
  6. Price updates in real-time
  7. "Add to Cart" enabled when valid

Kiosk Flow

  1. Customer taps item
  2. Full-screen customization
  3. Step through each modifier group
  4. Large touch targets for options
  5. Running total displayed
  6. Confirm and add to order

KDS Display

Cheeseburger
  - Size: Large
  - Extra Cheese
  - No Pickles
  - Add Bacon

Receipt Format

Cheeseburger                    €8.95
  Large                         €2.00
  Extra Cheese                  €1.50
  Bacon                         €2.00
                              --------
                               €14.45

Relations

Depends On

  • Nothing (standalone feature)

Affects

  • Menu Items: Items reference modifier groups
  • Order Total: Modifier prices affect total
  • KDS: Modifiers shown for preparation
  • Reports: Modifier popularity tracked

Business Rules

  • Pricing must be one of No charge (no-charge), Individual (individual), or Group (group). With Individual pricing each modifier's price is stored separately in a modifier_prices map on the group; with Group pricing every modifier is forced to the single group price; with No charge every modifier price is 0.
  • A tax_rate_code is only accepted when pricing is not No charge, and it is validated only when provided.
  • pos_name is optional and must be unique within the location; if blank, the group's name is used everywhere.
  • visibility is required and must contain at least one channel from: Kiosk, POS, Online Ordering, Table Qr Ordering, Uber Eats, Takeaway, Shopify.
  • Modifier groups are synced to all active third-party integrations (Square, MplusKassa, …) on create, update, and delete — not to Square alone. (Verified: upvendo-backend app/Services/BackOffice/ModifierGroupService.php lines 143-144, 169-170, 188-190.)
  • Creating a modifier group is rejected (422) when the location's connected POS is Hendrickx, Vanhoutte or ShopCaisse, and deleting one is rejected when the group's own label is ShopCaisse — those catalogs are read-only inbound. MplusKassa, Square and Lightspeed K-Series do not block modifier-group creation. (Verified: upvendo-backend config/pos-providers.php lines 36-39, 60-63, 94-104, 129-137.)
  • Modifier groups linked to Hendrickx, Vanhoutte, ShopCaisse, or Lightspeed K-Series have their Group Name, pricing (mode and prices), Tax Rate, all four Settings toggles/dropdown, and modifier list membership locked (read-only) in Upvendo, since those integrations own that data and would overwrite local edits on the next sync. MplusKassa-synced groups lock the same fields except settings.max_selected and tax_rate_code, which remain editable and sync back to MplusKassa. Description, POS Name, and Channels (visibility) are never locked by any integration.
  • Deleting a modifier group does not automatically remove it from items that reference it; items will simply no longer display the removed group.
  • The order field on a newly created modifier group is set to the count of existing groups, placing it last by default. The field is marked @deprecated in the model.
  • Modifier option images are uploaded and stored as content entities; the content_id is saved on the modifier record after media processing.
  • Modifier prices are normally not sent to MplusKassa when the modifier maps to an Mplus article — Mplus prices it from its own catalogue. Exceptions: text-only modifiers (no article number) always send their price, and on a per-person (bestelbeperking) line that Upvendo collapses for the POS — party size greater than 1, article units divisible by the party size, and a resolvable unit price — every priced modifier on that line is sent explicitly at party size × its price, because the collapsed line's quantity no longer carries the party size. MENU-group modifiers never send a price at all (their cost is inside the menu article's fixed price). When the line cannot be collapsed it is sent uncollapsed at full quantity and none of this applies. See Menu Items → Per-Person Items. (Verified: upvendo-backend app/Services/MplusKassa/OrderSyncService.php lines 744-767 and 843-860; app/Support/PerPersonLine.php.)

FAQs

  • What is the difference between modifiers and variants? Modifiers are add-on customizations (toppings, sauces) sharing the item's inventory. Variants are distinct product variations (sizes) with their own SKU and inventory tracking.

  • How is a modifier priced? Each modifier has an absolute price (not a +/- adjustment). The price actually applied depends on the group's pricing mode: No charge forces every price to 0, Group forces every modifier to the single group price, and Individual lets each modifier keep its own price.

  • Can I share one modifier group across multiple items? Yes. The same modifier group can be attached to many items; you attach it from each item's own editor.

  • "Where do I create and manage modifier groups?" Go to Menus → Modifier Groups (/menus/modifiers). Click "Create Modifier Group" to add one, or use the row actions to Edit or Delete an existing group.

  • "What's the difference between the Name and the POS Name fields?" "Modifier Group Name" is the customer-facing/internal name and is required. "POS Name" is optional, used for internal purposes only (e.g. a shorter name on the kitchen printer), and must be unique within the location. If left blank, the regular name is used everywhere.

  • "What pricing options does a modifier group have?" Three: No charge (modifiers are free), Individual (each modifier has its own price), and Group (all modifiers share one price set on the group). A tax rate can be set for Individual and Group pricing; it isn't required for No charge.

  • "If I pick Group pricing, can each modifier have a different price?" No. With Group pricing every modifier is forced to the single group price, and with No charge every modifier is set to 0. Choose Individual pricing if each option needs its own price.

  • "How do I let customers choose more than one modifier from a group?" In Settings, turn on "Allow customers to select more than one modifier." A "Maximum of modifiers a customer can select?" dropdown then appears (No Maximum or a specific number). Leaving the toggle off makes it single-choice.

  • "How do I make a group mandatory?" In Settings, set the selection type to Required (the default is Optional). Required means the customer must pick a modifier from this group.

  • "What is 'Allow customers to select the same modifier more than once'?" A Settings toggle that lets a customer add the same option multiple times (e.g. double sugar, double sauce) rather than just once.

  • "What's the 'Kitchen Name' on a modifier for?" Each modifier has an optional Kitchen Name alongside its customer-facing Name — use it to give kitchen/printer staff a clearer or shorter label.

  • "Which sales channels can a modifier group appear on?" In the Channels section you pick visibility per channel: Kiosk, POS, Online Ordering, Table Qr Ordering, Uber Eats, Takeaway, and Shopify. At least one channel must be selected.

  • "I attached a modifier group but it doesn't show on my item — where do I attach it?" On the modifier group page the "Items" section is read-only and just lists items already using the group. You attach a group to a product from the item's own editor, not from the modifier group screen.

  • "Why are my MplusKassa / ShopCaisse / Hendrickx / Vanhoutte / Lightspeed K-Series modifier group's fields greyed out?" Modifier groups synced from those POS/integration systems are partially or fully read-only. Their uneditable fields come from the source system and would be overwritten on the next sync, so they're locked (driven by ModifierGroup::getUneditableFields()). Lightspeed K-Series-linked groups are now fully locked too (its Menu API is read-only/GET-only, so the whole group shape is pulled down from Lightspeed) — the same field set as ShopCaisse. MplusKassa is the one exception: only settings.max_selected ("Maximum of modifiers a customer can select") stays editable there, because it syncs bi-directionally back to MplusKassa; the group's other settings stay locked. See the table below for exactly what's locked per system:

    Field / SettingHendrickx / Vanhoutte (Kassanet)ShopCaisseLightspeed K-SeriesMplusKassa
    Group NameLockedLockedLockedLocked
    Pricing mode & prices (pricing, modifier_prices)LockedLockedLockedLocked
    Tax Rate (tax_rate_code)LockedLockedLockedEditable
    Required / Allow Multiple / Allow Same (settings.is_mandatory, settings.allow_select_more_than_one, settings.allow_same_modifier_more_than_one)LockedLockedLockedLocked
    Maximum Selected (settings.max_selected)LockedLockedLockedEditable
    Add/remove modifier options (modifier_ids)LockedLockedLockedLocked
    Description, POS Name, Channels (Visibility)EditableEditableEditableEditable

    Description, POS Name, and Channels are never locked by any of these integrations — only the fields in the table above are affected. (Verified: upvendo-backend app/RawModels/ModifierGroup.php lines 325-390 — getUneditableFields(); the MplusKassa branch lists the settings sub-fields individually and omits both max_selected and tax_rate_code, while the Hendrickx/Vanhoutte, ShopCaisse and Lightspeed branches lock the whole settings block plus tax_rate_code.)

  • "Will my changes to a modifier group show up in online ordering right away?" Yes. Saving an edit fires a menu-reload so ordering channels pick up the change immediately, and create/update/delete are pushed to every active third-party integration (Square, MplusKassa, and so on).

  • "Why can't I create a new modifier group at all?" Some POS integrations own their catalog read-only, so Upvendo blocks local creation: Hendrickx, Vanhoutte and ShopCaisse locations get "This action isn't available while <POS> is your connected POS — its catalog is managed in <POS>." Create the group in the POS instead and it will sync into Upvendo. Deleting is blocked the same way for ShopCaisse groups.

  • "Can I add a photo to a modifier?" Yes. Each modifier option supports an image upload, used where modifier images are shown (e.g. kiosk).

  • "Why can I change 'Maximum of modifiers a customer can select' on my MplusKassa group but not the other settings?" MplusKassa is the one integration where settings.max_selected isn't locked — it's the only setting that syncs bi-directionally back to MplusKassa, so Upvendo lets you edit it. Required, Allow customers to select more than one modifier, Allow customers to select the same modifier more than once, the Group Name, pricing, and the modifier list itself all stay MplusKassa-owned and read-only, since MplusKassa would overwrite any local change on the next sync. Tax Rate also stays editable for MplusKassa groups.

  • "Is my Lightspeed K-Series modifier group editable in Upvendo?" No, not the group-level fields. Lightspeed K-Series' Menu API is read-only (GET-only), so the whole group — Group Name, pricing, Tax Rate, all Settings, and the modifier list — comes from the Lightspeed menu pull-down and is locked in Upvendo, the same as ShopCaisse. Only Description, POS Name, and Channels (visibility) stay editable.


Troubleshooting

Problem: Modifier group not showing on item

Causes:

  1. Group not attached to item
  2. Group has no modifiers
  3. Group's channel visibility excludes that channel

Solutions:

  1. Attach the group from the item's own editor
  2. Add modifiers to the group
  3. Check the group's Channels (visibility) selection

Problem: Customer can't add item to cart

Causes:

  1. A Required group (settings.is_mandatory) has no selection
  2. Validation error

Solutions:

  1. Check which group is set to Required
  2. Review the group's settings

Problem: Price not calculating correctly

Causes:

  1. Wrong pricing mode for the group
  2. A modifier's price is incorrect

Solutions:

  1. Confirm the pricing mode (No charge / Individual / Group) is what you intend
  2. Review each modifier's price (remember Group forces all to the group price, No charge forces all to 0)

Assistant Guidance

When answering questions about this feature:

  • If a merchant asks why a modifier group field is greyed out, first ask (or check) which POS/integration the group is synced from — the locked-field set differs per integration. Don't give a blanket "it's locked" answer; point to the specific fields for that integration using the table in the POS field-locking FAQ.
  • MplusKassa is the exception, not the rule: only settings.max_selected (and tax_rate_code) stay editable on MplusKassa-synced groups — everything else that's locked for other integrations is locked there too. Don't imply the whole group is editable just because one field is.
  • Lightspeed K-Series-synced modifier groups are now fully locked (as of the June 2026 change), the same as ShopCaisse. Do not tell a merchant a Lightspeed-synced group is editable.
  • Description, POS Name, and Channels (visibility) remain editable regardless of POS sync — if a merchant only needs to change one of those, there's no lock to work around.
  • To actually change a locked field, the merchant must make the change in the source POS system (MplusKassa, ShopCaisse, Hendrickx, Vanhoutte, Lightspeed K-Series) — it will sync into Upvendo on the next sync and overwrite any local edit anyway.

Examples

The JSON below mirrors the create/update request payload (details, pos_name, pricing, price, tax_rate_code, visibility, settings, modifiers[]). location_id is also required on the request but omitted here for brevity.

Size Selection (Required, single, individual pricing)

json
{
  "details": { "name": "Size", "description": "Choose your size" },
  "pos_name": "Size",
  "pricing": "individual",
  "tax_rate_code": "standard",
  "visibility": ["Kiosk", "POS", "Online Ordering"],
  "settings": {
    "is_mandatory": true,
    "allow_select_more_than_one": false,
    "allow_same_modifier_more_than_one": false
  },
  "modifiers": [
    { "name": "Small", "kitchen_name": "S", "price": 0 },
    { "name": "Medium", "kitchen_name": "M", "price": 1.50 },
    { "name": "Large", "kitchen_name": "L", "price": 3.00 }
  ]
}

Extra Toppings (Optional, multiple selection up to 3, individual pricing)

json
{
  "details": { "name": "Extra Toppings" },
  "pricing": "individual",
  "tax_rate_code": "standard",
  "visibility": ["Kiosk", "POS", "Online Ordering", "Takeaway"],
  "settings": {
    "is_mandatory": false,
    "allow_select_more_than_one": true,
    "max_selected": 3,
    "allow_same_modifier_more_than_one": false
  },
  "modifiers": [
    { "name": "Extra Cheese", "kitchen_name": "Cheese", "price": 1.50 },
    { "name": "Bacon", "kitchen_name": "Bacon", "price": 2.00 },
    { "name": "Mushrooms", "kitchen_name": "Mushr", "price": 1.00 },
    { "name": "Jalapeños", "kitchen_name": "Jala", "price": 0.75 }
  ]
}

Choose Your Sauce (Required, single, no charge)

json
{
  "details": { "name": "Choose Your Sauce" },
  "pricing": "no-charge",
  "visibility": ["Kiosk", "POS", "Online Ordering"],
  "settings": {
    "is_mandatory": true,
    "allow_select_more_than_one": false,
    "allow_same_modifier_more_than_one": false
  },
  "modifiers": [
    { "name": "Ketchup", "price": 0 },
    { "name": "Mayonnaise", "price": 0 },
    { "name": "BBQ Sauce", "price": 0 },
    { "name": "Hot Sauce", "price": 0 },
    { "name": "No Sauce", "price": 0 }
  ]
}

Add Extra Shot (Optional, group pricing — every modifier €0.80)

json
{
  "details": { "name": "Add Extra Shot" },
  "pricing": "group",
  "price": 0.80,
  "tax_rate_code": "standard",
  "visibility": ["Kiosk", "POS"],
  "settings": {
    "is_mandatory": false,
    "allow_select_more_than_one": true,
    "max_selected": 2,
    "allow_same_modifier_more_than_one": true
  },
  "modifiers": [
    { "name": "Espresso Shot", "kitchen_name": "Shot", "price": 0.80 },
    { "name": "Decaf Shot", "kitchen_name": "Decaf", "price": 0.80 }
  ]
}

Remove Ingredients (Optional, multiple selection, no charge)

json
{
  "details": { "name": "Remove Ingredients" },
  "pricing": "no-charge",
  "visibility": ["Kiosk", "POS", "Online Ordering"],
  "settings": {
    "is_mandatory": false,
    "allow_select_more_than_one": true,
    "max_selected": "No Maximum",
    "allow_same_modifier_more_than_one": false
  },
  "modifiers": [
    { "name": "No Lettuce", "price": 0 },
    { "name": "No Tomato", "price": 0 },
    { "name": "No Onion", "price": 0 },
    { "name": "No Pickles", "price": 0 }
  ]
}