Modifiers let customers customize menu items -- choosing bread type, adding toppings, selecting a preparation style. In the Tote API, modifiers are organized into modifier groups that can nest up to 3 levels deep, enabling complex customization trees like "Sandwich > Steak > Medium > Chimichurri Sauce."
This guide explains the modifier data model, walks through a fully annotated 3-level worked example, and covers pricing, validation, default selections, and UI rendering tips.
A modifier is a customization option for a menu item (e.g., "Cheddar Cheese", "Medium", "A1 Steak Sauce").
A modifier group is a named collection of modifiers with selection rules (e.g., "Bread Choice: pick exactly 1", "Toppings: pick up to 5").
Menu items contain modifier groups. Each modifier within a group can itself contain nested modifier groups, creating a tree structure. The API supports up to 3 levels of nesting.
Menu Item
|-- Modifier Group (Level 1)
| |-- Modifier
| | |-- Modifier Group (Level 2)
| | |-- Modifier
| | | |-- Modifier Group (Level 3)
| | | |-- Modifier
| | | |-- Modifier
| | |-- Modifier
| |-- Modifier
|-- Modifier Group (Level 1)
|-- Modifier
|-- ModifierEvery modifier group defines selection constraints using three fields:
| Field | Type | Description |
|---|---|---|
min_selections | integer | Minimum modifiers the customer must select. 0 = optional. |
max_selections | integer | Maximum modifiers the customer may select. |
allows_duplicates | boolean | Whether the same modifier can be selected more than once. |
| Pattern | min | max | duplicates | Example |
|---|---|---|---|---|
| Required single-select | 1 | 1 | false | Bread Choice (pick exactly 1) |
| Required multi-select | 1 | 3 | false | Protein (pick 1 to 2) |
| Optional multi-select | 0 | 5 | false | Toppings (pick up to 5) |
| Optional with duplicates | 0 | 3 | true | Extra Cheese (add up to 3x) |
| Required with duplicates | 1 | 10 | true | Scoops (pick 1 to 10, same flavor OK) |
This is the centerpiece of the modifier system. We will walk through a real menu item from the API with all 3 levels of nesting, showing the JSON at each level.
The "Build Your Own Sub Sandwich" has three modifier groups at Level 1:
{
"id": "d0000001-0000-0000-0000-000000000001",
"name": "Build Your Own Sub Sandwich",
"description": "Choose your bread, protein, and toppings.",
"base_price": { "amount": 899, "currency": "USD" },
"available": true,
"modifier_groups": [
{ "name": "Bread Choice", "..." : "Level 1 group" },
{ "name": "Protein", "..." : "Level 1 group" },
{ "name": "Toppings", "..." : "Level 1 group" }
]
}Base price: $8.99 (899 cents).
The customer must pick exactly one bread.
{
"id": "mg000001-0000-0000-0000-000000000001",
"name": "Bread Choice",
"min_selections": 1,
"max_selections": 1,
"allows_duplicates": false,
"modifiers": [
{
"id": "m0000001-0000-0000-0000-000000000001",
"name": "White",
"price_adjustment": { "amount": 0, "currency": "USD" },
"is_default": true,
"modifier_groups": []
},
{
"id": "m0000001-0000-0000-0000-000000000002",
"name": "Wheat",
"price_adjustment": { "amount": 0, "currency": "USD" },
"is_default": false,
"modifier_groups": []
},
{
"id": "m0000001-0000-0000-0000-000000000003",
"name": "Italian Herb & Cheese",
"price_adjustment": { "amount": 75, "currency": "USD" },
"is_default": false,
"modifier_groups": []
}
]
}Key observations:
min_selections: 1andmax_selections: 1-- the customer must pick exactly one bread.- White is
is_default: true-- pre-selected in the UI. - White and Wheat cost $0 extra. Italian Herb & Cheese adds $0.75 (75 cents).
modifier_groups: []-- no further nesting. This is a leaf-level group.
The customer must pick 1 to 2 proteins.
{
"id": "mg000001-0000-0000-0000-000000000002",
"name": "Protein",
"min_selections": 1,
"max_selections": 2,
"allows_duplicates": false,
"modifiers": [
{
"id": "m0000002-0000-0000-0000-000000000001",
"name": "Turkey",
"price_adjustment": { "amount": 0, "currency": "USD" },
"is_default": false,
"modifier_groups": []
},
{
"id": "m0000002-0000-0000-0000-000000000002",
"name": "Ham",
"price_adjustment": { "amount": 0, "currency": "USD" },
"is_default": false,
"modifier_groups": []
},
{
"id": "m0000002-0000-0000-0000-000000000003",
"name": "Steak",
"price_adjustment": { "amount": 200, "currency": "USD" },
"is_default": false,
"modifier_groups": [
{ "name": "Steak Preparation", "..." : "Level 2 group (see below)" }
]
}
]
}Key observations:
max_selections: 2-- the customer can pick up to two proteins (e.g., Turkey + Ham).- Turkey and Ham cost $0 extra. Steak adds $2.00 (200 cents).
- Steak has a nested modifier group -- selecting Steak unlocks "Steak Preparation" at Level 2.
- Turkey and Ham have
modifier_groups: []-- they are leaf-level modifiers.
When the customer selects Steak, they must choose a preparation level.
{
"id": "mg000002-0000-0000-0000-000000000001",
"name": "Steak Preparation",
"min_selections": 1,
"max_selections": 1,
"allows_duplicates": false,
"modifiers": [
{
"id": "m0000003-0000-0000-0000-000000000001",
"name": "Rare",
"price_adjustment": { "amount": 0, "currency": "USD" },
"is_default": false,
"modifier_groups": []
},
{
"id": "m0000003-0000-0000-0000-000000000002",
"name": "Medium",
"price_adjustment": { "amount": 0, "currency": "USD" },
"is_default": true,
"modifier_groups": [
{ "name": "Sauce", "..." : "Level 3 group (see below)" }
]
},
{
"id": "m0000003-0000-0000-0000-000000000003",
"name": "Well Done",
"price_adjustment": { "amount": 0, "currency": "USD" },
"is_default": false,
"modifier_groups": []
}
]
}Key observations:
- This is a Level 2 group -- nested inside the Steak modifier.
- Medium is
is_default: true-- pre-selected when the customer picks Steak. - All preparations cost $0 extra (the $2.00 upcharge is on the Steak modifier itself).
- Medium has a nested modifier group -- selecting Medium unlocks "Sauce" at Level 3.
- Rare and Well Done have
modifier_groups: []-- they are leaf-level.
When the customer selects Medium steak, they can optionally choose up to 2 sauces.
{
"id": "mg000003-0000-0000-0000-000000000001",
"name": "Sauce",
"min_selections": 0,
"max_selections": 2,
"allows_duplicates": false,
"modifiers": [
{
"id": "m0000004-0000-0000-0000-000000000001",
"name": "A1 Steak Sauce",
"price_adjustment": { "amount": 0, "currency": "USD" },
"is_default": false,
"modifier_groups": []
},
{
"id": "m0000004-0000-0000-0000-000000000002",
"name": "Peppercorn Ranch",
"price_adjustment": { "amount": 50, "currency": "USD" },
"is_default": false,
"modifier_groups": []
},
{
"id": "m0000004-0000-0000-0000-000000000003",
"name": "Chimichurri",
"price_adjustment": { "amount": 75, "currency": "USD" },
"is_default": false,
"modifier_groups": []
}
]
}Key observations:
- This is the deepest level (Level 3). No further nesting is allowed.
min_selections: 0-- sauces are optional.max_selections: 2-- the customer can pick up to 2 sauces.- A1 is free, Peppercorn Ranch adds $0.50, Chimichurri adds $0.75.
- All modifiers have
modifier_groups: []-- guaranteed at Level 3.
The customer can pick up to 5 toppings. This is a flat group with no nesting.
{
"id": "mg000001-0000-0000-0000-000000000003",
"name": "Toppings",
"min_selections": 0,
"max_selections": 5,
"allows_duplicates": false,
"modifiers": [
{
"id": "m0000005-0000-0000-0000-000000000001",
"name": "Lettuce",
"price_adjustment": { "amount": 0, "currency": "USD" },
"is_default": false,
"modifier_groups": []
},
{
"id": "m0000005-0000-0000-0000-000000000002",
"name": "Tomato",
"price_adjustment": { "amount": 0, "currency": "USD" },
"is_default": false,
"modifier_groups": []
},
{
"id": "m0000005-0000-0000-0000-000000000003",
"name": "Pickles",
"price_adjustment": { "amount": 0, "currency": "USD" },
"is_default": false,
"modifier_groups": []
},
{
"id": "m0000005-0000-0000-0000-000000000004",
"name": "Jalapenos",
"price_adjustment": { "amount": 50, "currency": "USD" },
"is_default": false,
"modifier_groups": []
},
{
"id": "m0000005-0000-0000-0000-000000000005",
"name": "Avocado",
"price_adjustment": { "amount": 150, "currency": "USD" },
"is_default": false,
"modifier_groups": []
}
]
}Key observations:
min_selections: 0-- toppings are entirely optional.- Most toppings are free. Jalapenos add $0.50. Avocado adds $1.50.
- No nesting -- all modifiers are leaf-level.
Build Your Own Sub Sandwich ($8.99 base)
|
|-- Bread Choice [pick exactly 1]
| |-- White (+$0.00) *default*
| |-- Wheat (+$0.00)
| |-- Italian Herb & Cheese (+$0.75)
|
|-- Protein [pick 1 to 2]
| |-- Turkey (+$0.00)
| |-- Ham (+$0.00)
| |-- Steak (+$2.00)
| |
| |-- Steak Preparation [pick exactly 1]
| |-- Rare (+$0.00)
| |-- Medium (+$0.00) *default*
| | |
| | |-- Sauce [pick 0 to 2]
| | |-- A1 Steak Sauce (+$0.00)
| | |-- Peppercorn Ranch (+$0.50)
| | |-- Chimichurri (+$0.75)
| |
| |-- Well Done (+$0.00)
|
|-- Toppings [pick 0 to 5]
|-- Lettuce (+$0.00)
|-- Tomato (+$0.00)
|-- Pickles (+$0.00)
|-- Jalapenos (+$0.50)
|-- Avocado (+$1.50)The total price of a customized item is the base price plus all selected modifier price adjustments. Modifiers at every level contribute to the total.
Formula:
total_price = base_price + sum(all selected modifier price_adjustments)All monetary values are in integer cents (USD).
A customer orders a sub with:
- Italian Herb & Cheese bread (+$0.75)
- Steak protein (+$2.00)
- Medium preparation (+$0.00)
- Chimichurri sauce (+$0.75)
- Lettuce (+$0.00)
- Avocado (+$1.50)
Base price: $8.99 (899 cents)
+ Italian Herb & Cheese: $0.75 ( 75 cents)
+ Steak: $2.00 (200 cents)
+ Medium: $0.00 ( 0 cents)
+ Chimichurri: $0.75 ( 75 cents)
+ Lettuce: $0.00 ( 0 cents)
+ Avocado: $1.50 (150 cents)
----------------------------------------------
Total: $13.99 (1399 cents)Pseudocode:
def calculate_item_price(item, selected_modifiers):
"""
Calculate total price for an item with selected modifiers.
item: MenuItem object
selected_modifiers: list of Modifier objects (all levels flattened)
"""
total = item["base_price"]["amount"] # Start with base price in cents
for modifier in selected_modifiers:
total += modifier["price_adjustment"]["amount"]
return totalNote: This is a client-side estimate. Use the server-side price calculation endpoint (Phase 2) for the authoritative total, which includes tax and discounts.
Modifiers with is_default: true are pre-selected in the UI. Defaults provide a reasonable starting configuration without requiring the customer to make every choice.
Behavior rules:
- Default modifiers count toward
min_selections. Ifmin_selections: 1and a modifier isis_default: true, the requirement is met without customer action. - A group can have at most
max_selectionsdefaults. (If a group allows 1 selection and 2 modifiers are marked default, use only the first.) - Default modifiers contribute to the price. If a default modifier has a non-zero
price_adjustment, it is included in the price from the start. - The customer can deselect defaults and choose alternatives.
In the worked example:
- Bread Choice: "White" is default (customer starts with White bread).
- Steak Preparation: "Medium" is default (if customer picks Steak, Medium is pre-selected).
- No defaults in Protein, Toppings, or Sauce groups.
Before submitting modifier selections to the cart, validate them client-side to provide immediate feedback.
def validate_modifier_group(group, selections):
"""
Validate modifier selections for a group.
group: ModifierGroup object
selections: list of selected modifier IDs for this group
Returns: (is_valid, error_message)
"""
count = len(selections)
min_sel = group["min_selections"]
max_sel = group["max_selections"]
# Check minimum selections
if count < min_sel:
return (False, f"{group['name']}: select at least {min_sel} (got {count})")
# Check maximum selections
if count > max_sel:
return (False, f"{group['name']}: select at most {max_sel} (got {count})")
# Check for duplicates
if not group["allows_duplicates"] and len(set(selections)) != count:
return (False, f"{group['name']}: duplicate selections not allowed")
# Check that selected IDs exist in the group
valid_ids = {m["id"] for m in group["modifiers"]}
for sel in selections:
if sel not in valid_ids:
return (False, f"{group['name']}: modifier {sel} not found in group")
return (True, None)
def validate_item_modifiers(item, all_selections):
"""
Validate all modifier selections for a menu item recursively.
item: MenuItem object
all_selections: dict mapping modifier_group_id -> list of selected modifier IDs
Returns: list of (is_valid, error_message) tuples
"""
errors = []
def validate_groups(groups):
for group in groups:
group_id = group["id"]
selections = all_selections.get(group_id, [])
is_valid, error = validate_modifier_group(group, selections)
if not is_valid:
errors.append(error)
# Recursively validate nested groups for selected modifiers
for modifier in group["modifiers"]:
if modifier["id"] in selections and modifier.get("modifier_groups"):
validate_groups(modifier["modifier_groups"])
validate_groups(item.get("modifier_groups", []))
return errors| Rule | Check | Error |
|---|---|---|
| Minimum selections | len(selections) >= min_selections | "Select at least N" |
| Maximum selections | len(selections) <= max_selections | "Select at most N" |
| No duplicates | If allows_duplicates: false, all IDs must be unique | "Duplicate selections not allowed" |
| Valid modifier IDs | All selected IDs exist in the group's modifiers array | "Modifier not found" |
| Nested groups | If a selected modifier has nested groups, validate those too | (recursive) |
| Level 3 is terminal | Modifiers at Level 3 will never have modifier_groups | N/A (enforced by API) |
Do not show all 3 levels at once. Reveal nested modifier groups only after the parent modifier is selected:
- Show Level 1 groups when the customer opens the item.
- When the customer selects a modifier with nested groups, expand Level 2 below it.
- When the customer selects a Level 2 modifier with nested groups, expand Level 3.
| Pattern | min | max | UI Element |
|---|---|---|---|
| Exactly 1 | 1 | 1 | Radio buttons |
| Up to 1 | 0 | 1 | Radio buttons + "None" option |
| Multiple | 0+ | 2+ | Checkboxes |
| With duplicates | any | any | Stepper/counter per modifier |
Display the price adjustment next to each modifier name so the customer knows the cost before selecting:
Bread Choice (pick 1):
(*) White
( ) Wheat
( ) Italian Herb & Cheese (+$0.75)Modifiers with price_adjustment.amount: 0 should show nothing or "Included" -- not "+$0.00."
When rendering a modifier group, check each modifier's is_default field. Pre-select those modifiers in the UI so the customer starts with a valid configuration.
Mark groups with min_selections >= 1 as "Required" in the UI. Groups with min_selections: 0 are optional and should be visually distinct.
Bread Choice (Required - pick 1):
...
Toppings (Optional - up to 5):
...- Getting Started Guide -- Your first API calls.
- Menu Synchronization Guide -- Keeping modifier data current.
- Authentication Guide -- Token management for API access.