Skip to main content

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

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​

  1. Direct Object References: Each entity (menu, category, item, etc.) contains its complete data structure
  2. Nested Hierarchy: Follows a natural menu organization pattern
  3. 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_id pointing 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​
  1. Data Duplication: Common modifiers or variations may be repeated across multiple items
  2. Deep Nesting: Complex items with multiple modifier groups and nested modifiers create deep object structures
  3. 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

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​

  1. Reference-Based Associations: Uses IDs to reference related entities instead of nesting complete objects
  2. Centralized Data Storage: Stores each entity type in its own array at the root level
  3. 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:

  1. 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 ItemModifierOverride array
    • Maintains price consistency with size variations
  2. 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)
  3. 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_free marks the group's default modifier options as not charged; absent or false means they are charged as normal

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.

EntityExample stream_id
Topping (fixed price)mod_pepperoni
Portion group under Pepperonimg_portions__mod_pepperoni
"1st Half" under Pepperonimod_half_1__mod_pepperoni
"1st Half" under Grilled Chickenmod_half_1__mod_chicken
"Extra" under "1st Half" under Pepperonimod_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:

SelectionResolved topping pricePublished price of the last selection
Pepperoni10001000
Pepperoni → Whole1000 × 1 = 10000
Pepperoni → 1st Half1000 × ½ = 500-500
Pepperoni → 1st Half → Extra500 × 2 = 1000500
Pepperoni → 1st Half → Light500 × ½ = 250-250
Pepperoni → Whole → Extra1000 × 2 = 20001000

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 LargeSourcePrice
Pepperoniitem_pizza_large.modifier_overrides1200
1st Halfitem_pizza_large.modifier_overrides-600
Extraitem_pizza_large.modifier_overrides600
On the Sidemod_side.price (no row)50
caution

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_id values without a length limit and without parsing them.
  • Accept a negative modifier price and a negative price_amount.
  • Add the delta of each selected modifier to the running total; do not multiply.
  • Apply modifier_overrides rows 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 schedule defining availability hours by day of week, using the same weekly time-slot shape as the menu-level schedule (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 schedule is 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 schedule consistently.

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:

  • vegan
  • vegetarian
  • halal
  • paleo
  • keto
  • gluten_free
  • dairy_free
  • kosher
  • spicy
  • nut_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 (ItemV2 and ModifierV2).
  • 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). The unit is 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
}
]
}
note

Nutrition, allergen, and dietary values reflect the base catalog object and are not overridable per menu.

See full schema

Data Organization​

Hierarchical References​

  1. Menus reference Categories (category_ids)
  2. Categories reference Item Families (item_family_ids)
  3. Item Families reference:
    • Items (item_ids)
    • Modifier Groups (modifier_group_ids)
  4. Modifier Groups reference:
    • Modifiers (modifier_ids)
    • Override Rules for specific contexts
  5. 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​

  1. Reduced Duplication: Common elements stored once and referenced
  2. Flat Structure: Minimizes nesting depth
  3. 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 base name field stores the default value.
  • description_translations — additional translated descriptions. The base description field remains the default-language value.

Each entry is a LocalizedName object:

FieldTypeDescription
localestringBCP 47 language tag (e.g. en, zh-Hant, fr).
valuestringThe 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": "香辣花生炒雞肉" }
]
}
note

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.

See full schema