Menu Schemas
Full schema descriptions can be found here
Both menu schema types share the following:
- Schedule Management: Detailed weekly schedule support with multiple time slots per day
- Holiday Handling: Flexible holiday definitions with recurring and specific date options
- Tax Management: Global tax definitions with item-level application
Menu Schema V1
Overview
The V1 menu schema implements a straightforward, hierarchical structure for representing menu data. While intuitive and easy to understand, this schema can be memory-intensive due to its nested approach and potential data redundancy.
Schema Structure
Core Components
The schema consists of several key entities organized in a hierarchical structure:
DspLocationMenuEventDataDto
├── Menus[] # Top-level containers
├── Categories[] # Groups of related items
├── Items[] # Individual products
├── Variations[] # Different versions of items
├── ModifierGroups[] # Customization options for items
├── Modifiers[] # Specific modifications
└── NestedModifierGroups[] # Nested customization options
Key Features
- Direct Object References: Each entity (menu, category, item, etc.) contains its complete data structure
- Nested Hierarchy: Follows a natural menu organization pattern
- Self-Contained Objects: Each object contains all its relevant data without relying on external references (except for tax IDs)
Data Organization
- Menus: Top-level containers that include categories and scheduling information
- Categories: Groups of related items (e.g., "Appetizers", "Main Courses")
- Items: Individual products with variations and modifier options. May include a
default_item_idpointing to the default variation when configured. - Modifier Groups: Customization options for items
- Holidays & Taxes: Stored at the root level for global access
Memory Considerations
Memory Intensive Aspects
- Data Duplication: Common modifiers or variations may be repeated across multiple items
- Deep Nesting: Complex items with multiple modifier groups and nested modifiers create deep object structures
- Complete Object Storage: Each entity stores its full data structure, leading to larger payload sizes
Use Cases
This schema is well-suited for:
- Simple menu structures
- Small to medium-sized menus
- Scenarios where direct access to complete object data is prioritized over memory efficiency
- Development environments where memory optimization is not a primary concern
Implementation Notes
- All monetary values are stored in lowest denomination (cents)
- Currency codes follow ISO 4127 standards
- Time slots support overnight hours (crossing midnight)
- Tax rates can be marked as inclusive or exclusive
Menu Schema V2
Overview
The V2 menu schema implements an association-based structure that significantly reduces payload size through the use of references and centralized data storage. This schema introduces important optimizations and new features like modifier price overrides, while maintaining all the functionality of V1.
Schema Structure
Core Components
The schema uses a flat structure with associations:
DspLocationMenuEventDataV2Dto
├── Menus[] # Top-level containers
├── Categories[] # Centralized category definitions
├── ItemFamilies[] # Groups of related items
├── Items[] # Centralized item definitions
├── ModifierGroups[] # Centralized modifier group definitions
└── Modifiers[] # Centralized modifier definitions
Key Optimizations
- Reference-Based Associations: Uses IDs to reference related entities instead of nesting complete objects
- Centralized Data Storage: Stores each entity type in its own array at the root level
- Reduced Redundancy: Common elements are defined once and referenced multiple times
New Features
Enriched Override Information
A significant enhancement in V2 is the ability to override modifier prices based on context:
Item-Specific Pricing: Modifiers can have different prices when applied to specific items
- Example: Adding cheese might cost more on a large pizza vs. a small pizza
- Implemented through
ItemModifierOverridearray - Maintains price consistency with size variations
Menu-Specific Overrides: Items can have different prices and availability in different menus
- Controlled through type
MenuSpecificItemOverride - Supports price variations across different menus
- Includes activation control on a per menu basis (
is_active)
- Controlled through type
Modifier Group Rules Overrides: Modifier groups can have different rules for different parent contexts
- Controlled through type
ModifierGroupRuleOverride - Supports rules across different parent contexts (ex: different modifier groups for different items)
- Includes default modifier control on a per parent context basis (
default_modifier_quantities) default_modifiers_freemarks the group's default modifier options as not charged; absent orfalsemeans they are charged as normal
- Controlled through type
Portion and Multiplier Modifiers
Some POS systems price a modifier as a multiple of the option it sits under, not as a fixed amount. Typical cases are a half portion of a pizza topping ("1st Half" = ½ of the topping price) and pre-modifiers such as "Extra" (×2) or "Light" (×½). The price of such a modifier depends on its parent, and the same "1st Half" can sit under toppings with different prices.
The V2 schema lists each modifier once with one price, so Stream publishes a multiplier modifier once per parent path, each with its own stream_id and its own price.
This applies to the V2 menu schema only. V1 menus are unchanged. A menu with no multiplier modifiers is published exactly as before.
Published IDs
A multiplier modifier, and the modifier group that holds it, carry an ID that is unique to the path they sit on. Fixed-price modifiers keep their plain ID, except the free copy of a default described under Free Default Options. A multiplier modifier attached directly to an item, with no modifier above it, keeps its plain ID and its POS price.
| Entity | Example stream_id |
|---|---|
| Topping (fixed price) | mod_pepperoni |
| Portion group under Pepperoni | mg_portions__mod_pepperoni |
| "1st Half" under Pepperoni | mod_half_1__mod_pepperoni |
| "1st Half" under Grilled Chicken | mod_half_1__mod_chicken |
| "Extra" under "1st Half" under Pepperoni | mod_extra__mod_half_1__mod_pepperoni |
| "On the Side" (fixed price, under any group) | mod_side |
Treat every stream_id as an opaque string:
- Do not parse it, shorten it, or rebuild it from its parts. The format can change.
- Do not limit its length. An ID grows with each multiplier level and can be well over 100 characters.
- The same POS modifier can be published under several IDs. Each one is a separate modifier on your side.
- A modifier that was published under its plain ID before this change (for example
mod_half_1) may no longer be published under that ID once it sits under another modifier. Replace your stored menu on every publish; do not keep IDs from an earlier publish.
Prices Are Deltas
The price of a multiplier modifier is the change it makes to its parent's price, in the lowest denomination. It is not the price of the portion. It can be negative.
For a Pepperoni topping at 1000:
| Selection | Resolved topping price | Published price of the last selection |
|---|---|---|
| Pepperoni | 1000 | 1000 |
| Pepperoni → Whole | 1000 × 1 = 1000 | 0 |
| Pepperoni → 1st Half | 1000 × ½ = 500 | -500 |
| Pepperoni → 1st Half → Extra | 500 × 2 = 1000 | 500 |
| Pepperoni → 1st Half → Light | 500 × ½ = 250 | -250 |
| Pepperoni → Whole → Extra | 1000 × 2 = 2000 | 1000 |
The customer pays the sum of the selected lines: Pepperoni, 1st Half and Extra come to 1000 + (-500) + 500 = 1000. Stream rounds the resolved price at each step to a whole number of the lowest denomination and publishes the difference, so the selected lines always add up to the rounded price.
An excerpt of the published menu:
{
"modifier_groups": [
{
"stream_id": "mg_toppings",
"name": "Toppings",
"modifier_ids": ["mod_pepperoni", "mod_chicken"]
},
{
"stream_id": "mg_portions__mod_pepperoni",
"name": "Portion",
"rules": {
"minimum_unique_modifiers_allowed": 1,
"maximum_unique_modifiers_allowed": 1,
"selection_type": "single_proportion",
"default_modifier_quantities": { "mod_whole__mod_pepperoni": 1 }
},
"modifier_ids": [
"mod_whole__mod_pepperoni",
"mod_half_1__mod_pepperoni",
"mod_half_2__mod_pepperoni"
]
},
{
"stream_id": "mg_premod__mod_half_1__mod_pepperoni",
"name": "Extra / Light",
"modifier_ids": [
"mod_extra__mod_half_1__mod_pepperoni",
"mod_light__mod_half_1__mod_pepperoni",
"mod_side"
]
}
],
"modifiers": [
{
"stream_id": "mod_pepperoni",
"name": "Pepperoni",
"price": 1000,
"currency": "usd",
"nested_modifier_group_ids": ["mg_portions__mod_pepperoni"]
},
{
"stream_id": "mod_half_1__mod_pepperoni",
"name": "1st Half",
"price": -500,
"currency": "usd",
"nested_modifier_group_ids": ["mg_premod__mod_half_1__mod_pepperoni"]
},
{
"stream_id": "mod_extra__mod_half_1__mod_pepperoni",
"name": "Extra",
"price": 500,
"currency": "usd",
"nested_modifier_group_ids": []
},
{
"stream_id": "mod_light__mod_half_1__mod_pepperoni",
"name": "Light",
"price": -250,
"currency": "usd",
"nested_modifier_group_ids": []
},
{
"stream_id": "mod_side",
"name": "On the Side",
"price": 50,
"currency": "usd",
"nested_modifier_group_ids": []
}
]
}
Before this change the same menu published one mod_half_1 and one mod_extra, each with price: 0, so a half portion and an "Extra" were shown as free.
Per-Size Prices
When a topping costs more on a larger size, its portions do too. ItemV2.modifier_overrides already carried the topping's price for that item. It now also carries a row for each multiplier modifier nested under that topping whose delta differs on that size.
Pepperoni is 800 on a Small and 1200 on a Large. The rows for the "1st Half" path are shown; the other paths ("Whole", "2nd Half") get rows in the same way:
{
"items": [
{
"stream_id": "item_pizza_small",
"name": "Small",
"price": 800,
"currency": "usd",
"modifier_overrides": [
{ "modifier_id": "mod_pepperoni", "price_amount": 800 },
{ "modifier_id": "mod_half_1__mod_pepperoni", "price_amount": -400 },
{
"modifier_id": "mod_extra__mod_half_1__mod_pepperoni",
"price_amount": 400
},
{
"modifier_id": "mod_light__mod_half_1__mod_pepperoni",
"price_amount": -200
}
]
},
{
"stream_id": "item_pizza_large",
"name": "Large",
"price": 1200,
"currency": "usd",
"modifier_overrides": [
{ "modifier_id": "mod_pepperoni", "price_amount": 1200 },
{ "modifier_id": "mod_half_1__mod_pepperoni", "price_amount": -600 },
{
"modifier_id": "mod_extra__mod_half_1__mod_pepperoni",
"price_amount": 600
},
{
"modifier_id": "mod_light__mod_half_1__mod_pepperoni",
"price_amount": -300
}
]
}
]
}
To price a selection on an item, look up each selected modifier's stream_id in that item's modifier_overrides, at every nesting level. Use the row's price_amount when there is one, and the modifier's own price when there is not.
| Selection on a Large | Source | Price |
|---|---|---|
| Pepperoni | item_pizza_large.modifier_overrides | 1200 |
| 1st Half | item_pizza_large.modifier_overrides | -600 |
| Extra | item_pizza_large.modifier_overrides | 600 |
| On the Side | mod_side.price (no row) | 50 |
If you apply modifier_overrides to top-level modifiers only, a half of Pepperoni on a Large is priced with the base delta (-500) where the Large's row says -600. The customer pays 1200 + (-500) = 700 where the merchant's POS charges 600. See Portion and Multiplier Modifiers on Orders.
Free Default Options
When a modifier group's default options are free (default_modifiers_free), the portions under a free default are published with price: 0, because a fraction of a free topping is also free. An upcharge such as "Extra" (×2) keeps its price.
A topping with portion options that is a free default in one group and a paid option in another group of the menu is published once under its plain ID, and once more for each item family that lists it in a free group, under a second ID. That free group gets a second ID too. Both modifiers carry the same price; the group's default_modifiers_free is what makes the copy free. Both are ordinary modifiers; return whichever one the customer selected.
Checklist
- Store
stream_idvalues without a length limit and without parsing them. - Accept a negative modifier
priceand a negativeprice_amount. - Add the delta of each selected modifier to the running total; do not multiply.
- Apply
modifier_overridesrows to nested modifiers, not only to top-level ones. - Expect more modifiers and modifier groups for the same menu, and a larger payload. Each topping gains one modifier per portion option and per pre-modifier under it, so a pizza menu can double in size or more.
- On orders, return the published IDs and prices unchanged. See Portion and Multiplier Modifiers on Orders.
Item Families
- Introduces the concept of item families for better organization
- Groups related items with shared characteristics
- Contains common attributes like:
- Description
- Images
- Modifier groups
- Tax assignments
- Alcohol designation
- Default item ID (when a default variation is configured)
Category-Level Availability Hours
- Categories may include an optional
scheduledefining availability hours by day of week, using the same weekly time-slot shape as the menu-levelschedule(multiple slots per day, overnight slots supported). - This lets partners that support category-level scheduling natively honor per-category hours (e.g. a "Lunch" category available only midday).
- The
scheduleis omitted when a category has no defined hours; those categories follow the existing menu-level availability behavior, so current consumers are unaffected. - A category that appears across multiple menus carries its own catalog-level
scheduleconsistently.
Nutrition & Dietary Information
V2 menus can carry nutrition, allergen, and dietary metadata so you can surface richer product information to diners. All of these fields are optional — populate the ones your platform supports and ignore the rest.
Dietary tags (dietary_tags) are available on item families (ItemFamilyV2) and modifiers (ModifierV2). They describe dietary suitability and accept the following values:
veganvegetarianhalalpaleoketogluten_freedairy_freekosherspicynut_free
Allergens (allergens) were already available on item families and are now also available on modifiers (ModifierV2), using the same allergen enum (e.g. eggs, dairy, gluten, peanuts).
Per-variation nutrition lives on items (ItemV2, a variation of an item family) and on modifiers (ModifierV2):
calories— caloric content in kcal (ItemV2andModifierV2).ingredients— an ordered list of ingredient strings (ItemV2).alcohol_percentage— alcohol by volume percentage (ItemV2).caffeine_content_mg— caffeine content in milligrams (ItemV2).additional_nutritional_details— a map keyed by nutrient name, where each value is a{ value, unit }pair (ItemV2). Theunitis one of the supported units of measure (e.g.g,mg,oz).
{
"item": [
{
"stream_id": "soda_12oz",
"name": "Cola 12oz",
"price": 250,
"currency": "usd",
"calories": 150,
"ingredients": ["carbonated water", "sugar", "natural flavors"],
"alcohol_percentage": 0,
"caffeine_content_mg": 34,
"additional_nutritional_details": {
"carbohydrates": { "value": 39, "unit": "g" },
"sodium": { "value": 45, "unit": "mg" }
}
}
],
"modifier": [
{
"stream_id": "extra_cheese",
"name": "Extra Cheese",
"price": 100,
"currency": "usd",
"allergens": ["dairy"],
"dietary_tags": ["vegetarian"],
"calories": 50
}
]
}
Nutrition, allergen, and dietary values reflect the base catalog object and are not overridable per menu.
Data Organization
Hierarchical References
- Menus reference Categories (
category_ids) - Categories reference Item Families (
item_family_ids) - Item Families reference:
- Items (
item_ids) - Modifier Groups (
modifier_group_ids)
- Items (
- Modifier Groups reference:
- Modifiers (
modifier_ids) - Override Rules for specific contexts
- Modifiers (
- Modifiers can reference nested Modifier Groups (
nested_modifier_group_ids)
Override System
- Item Level: Modifier price overrides based on specific items (ex different item sizes)
- Menu Level: Item price and availability overrides per menu (ex dinner menu has higher prices than a lunch menu)
- Modifier Group Level: Rule overrides based on parent context (ex Some items may have rule set of max modifiers allowed)
Memory Efficiency
- Reduced Duplication: Common elements stored once and referenced
- Flat Structure: Minimizes nesting depth
- Efficient Updates: Modifications to shared elements automatically reflect across all references
Implementation Considerations
- All monetary values remain in lowest denomination
- Maintains ISO 4127 currency code standards
- Preserves V1 functionality while adding new features
- Backwards compatible with appropriate data transformation
Localized Names & Descriptions
Menus, categories, item families, items (variations), modifier groups, and modifiers can carry translated names and descriptions so you can present bilingual or multilingual menus to diners. These fields are available on both the V1 and V2 schemas and are always optional — the base name and description remain the default-language values, and the translation arrays hold any additional locales.
name_translations— additional translated names. The basenamefield stores the default value.description_translations— additional translated descriptions. The basedescriptionfield remains the default-language value.
Each entry is a LocalizedName object:
| Field | Type | Description |
|---|---|---|
locale | string | BCP 47 language tag (e.g. en, zh-Hant, fr). |
value | string | The translated text for that locale. |
{
"stream_id": "kung_pao_chicken",
"name": "Kung Pao Chicken",
"name_translations": [{ "locale": "zh-Hant", "value": "宮保雞丁" }],
"description": "Spicy stir-fried chicken with peanuts",
"description_translations": [
{ "locale": "zh-Hant", "value": "香辣花生炒雞肉" }
]
}
Locales are deduplicated and case-insensitive; entries with an empty locale or value are dropped. A translation array is omitted entirely when no valid entries remain.