CatalogSet Structure
A CatalogSet is the complete menu for a store — every catalog, section, item, and modifier group in a single response. This guide covers the data model, business rules, and pricing logic you need to render menus and build carts correctly.
Structure Overview
CatalogSets use a normalized dictionary structure — flat id-to-object maps rather than deep nesting.
Why dictionaries?
- O(1) lookups — Find any entity instantly by ID
- No duplication — An item referenced by multiple sections exists once
- Flexible relationships — Modifiers reference items; items reference modifier groups; the graph can nest arbitrarily
Each dictionary key matches the entity's own ID property (e.g., catalogs["abc"].catalogId === "abc").
Navigating the Graph
Start from a catalog and follow ID references through the dictionaries:
Code
Build a section tree by starting with a catalog's sectionIds and recursively following sections[id].sectionIds. Use a visited set during recursion to guard against cycles.
Items and sub-sections are separate ordered lists within a section. There is no merged interleaved order — render items first, then child sections (or vice versa, per your UX).
Catalogs & Availability
A store can have multiple catalogs for different dayparts — Breakfast, Lunch, Dinner, Late Night.
Each catalog has an optional availability array of time windows:
| Field | Type | Description |
|---|---|---|
dayOfWeek | string | "Sunday" through "Saturday" |
start | string | Start time in HH:mm:ss format |
end | string | End time in HH:mm:ss format |
Rules:
- Null or empty availability = the catalog is never open. To declare 24/7 availability, provide an explicit window for each day-of-week. The same fail-closed semantic applies to
Store.availability. 00:00:00end time = end of day (not midnight-to-midnight)- Overnight windows must be split into two entries:
Code
Sections
Sections organize items into categories (e.g., "Appetizers", "Entrees") and support nesting.
| Field | Description |
|---|---|
itemIds | Ordered list of item IDs in this section |
sectionIds | Child section IDs for hierarchies (e.g., Food → Hot Food → Pizza) |
Key behaviors:
- A section can contain both items and nested sub-sections
- The same item can appear in multiple sections (shared items)
- Ordering within
itemIdsandsectionIdsis meaningful — use it for display order
Items
Items serve a dual role: they are both menu products customers can order and modifier options within modifier groups.
| Field | Description |
|---|---|
basePrice | Price before modifiers. For modifier options, this is the upcharge (e.g., Large = +$1.00) |
startingAt | Headline "from $X" display price only — the cheapest orderable configuration. It already includes the cheapest required modifier, so never add modifiers on top of it. The running/cart total is always computed from basePrice (see Price Calculation) |
compareAt | Original/marketplace price for showing savings (strikethrough pricing) |
isAvailable | Whether item can currently be ordered |
modifierGroupIds | Customization groups attached to this item |
:::note Modifiers are items
A "Small" size option and a "Margherita Pizza" are both Item objects. The difference is context — items in modifierGroups[id].itemIds are modifier options; items in sections[id].itemIds are menu products. Some items may be both.
:::
Modifier Groups
Modifier groups define customization options for an item — sizes, toppings, sides, etc.
Selection Rules
The minimumAllowed and maximumAllowed fields define what customers must select:
| Min | Max | Meaning | Example |
|---|---|---|---|
| 0 | 1 | Optional single choice | "Add a side?" |
| 1 | 1 | Required single choice (radio) | "Choose size: S / M / L" |
| 0 | N | Optional, up to N | "Add up to 5 toppings" |
| 1 | N | Required, 1 to N | "Pick 1-3 sauces" |
| N | N | Exactly N required | "Pick 2 sides" |
Validation rules:
minimumAllowed >= 0maximumAllowed >= minimumAllowed- When
enableDuplicateItemsis false,itemIds.length >= minimumAllowed(enough unique options to satisfy the minimum)
Duplicate Selections
Set enableDuplicateItems: true to allow the same option to be selected multiple times. This is essential for scenarios like:
Code
Without enableDuplicateItems, you would need 13 individual modifier groups for the Baker's Dozen item.
Default Items
The defaultItems array pre-selects modifier options (e.g., "Comes with lettuce and tomato"):
Code
Rules:
- Each
itemIdmust exist in the modifier group'sitemIds - Total quantity across all defaults must not exceed
maximumAllowed
Nested Modifiers
Because modifier options are items, they can have their own modifierGroupIds — enabling multi-level customization:
Code
Most menus stay 1-2 levels deep, but the structure supports arbitrary nesting.
Variant Size Groups
Multi-variant items (Small / Medium / Large; Thin Crust / Stuffed Crust; etc.) surface as a parent item plus a required single-select modifier group containing one item per variant:
Code
Conventions:
parent.basePrice = 0. The parent contributes nothing to the cart total — the absolute price comes entirely from the selected variant.parent.startingAtcarries the cheapest variant's price so menu rendering can show "from $8.00".- Each variant child carries its absolute price as
basePrice. This is the same value the partner echoes back when the user selects that variant. - The group's
minimumAllowedandmaximumAllowedare both1(radio-style required pick). - The variant child is a regular
Itemand may carry its own nestedmodifierGroupIds(per-variant toppings, for example).
Cart math is the standard additive model (no special-casing required): parent.basePrice (0) + the selected variant's basePrice is the absolute price of the chosen variant. Picking Medium Milkshake gives $0.00 + $9.00 = $9.00. This matches the partner-side semantics where a variant selector replaces — rather than adds on top of — the parent's price.
Tiered Pricing
Modifier groups can use tiered pricing instead of item base prices. When tieredPricing is present, it overrides basePrice for selections in that group.
Each tier has an offset (0-based selection index) and a price:
Code
How to resolve the tier for the k-th selection (0-based): use the tier with the greatest offset that is ≤ k.
| Selection | k | Applicable Tier | Price |
|---|---|---|---|
| 1st dish | 0 | offset 0 | $0.00 |
| 2nd dish | 1 | offset 0 | $0.00 |
| 3rd dish | 2 | offset 2 | $8.00 |
| 4th dish | 3 | offset 2 | $8.00 |
| 5th dish | 4 | offset 4 | $7.00 |
| 6th dish | 5 | offset 4 | $7.00 |
Tier rules:
- Offsets must be non-negative and unique
- Tiers must be in ascending offset order
- When
tieredPricingis null or empty, fall back to each item'sbasePrice
Price Calculation
Line item price = (base price + all modifier prices) × quantity. Modifier costs apply per-unit of the parent line — a qty-3 burger with a qty-1 cheese modifier costs 3 × ($10 + $1.50) = $34.50. Modifier groups with tieredPricing use the per-selection tier price in place of each selection's basePrice; nested modifiers below a tiered selection still contribute their own costs.
Headline "from" price vs running total
These are two different numbers — keep them separate:
- Headline / "from $X" (
startingAt) is a display projection: the cheapest orderable configuration, i.e.basePriceplus the cheapest option of each required group. Use it on cards and as the modal's opening price. It is not an input to any total. - Running / cart total is computed by the algorithm below, always from raw
basePrice(basePrice + Σ selected modifier basePrice). Never compute it asstartingAt + modifiers—startingAtalready folds in the cheapest required modifier, so adding modifiers on top double-counts.
Worked example — front-loaded "sundae" (parent basePrice $0, startingAt $10):
Code
| User picks | Running total (from raw) | ✗ Wrong (startingAt + modifier) |
|---|---|---|
| Small | $0 + $10 = $10 | $10 + $10 = $20 |
| Medium | $0 + $12 = $12 | $10 + $12 = $22 |
| Large | $0 + $14 = $14 | $10 + $14 = $24 |
The per-option display delta ($0 / +$2 / +$4) is purely a UI label — subtract the cheapest required option's price so the cheapest reads $0. The cart total is still computed from raw basePrice by the algorithm below, so the two never disagree.
The reference implementation is shipped as a stateless helper in @gett-co/ordering-core (cartMath.ts) and mirrored byte-for-byte in the .NET internal provider. Both are driven by a shared JSON fixture suite — partners are free to reuse the TypeScript module directly or port the algorithm into their own client:
Code
tierPriceAt(tiers, k) returns the tier with the greatest offset that is <= k.
Worked Examples
Burger with cheese (qty > 1)
| Line | Calculation | Subtotal |
|---|---|---|
| 3× Burger ($10) + 1× Cheese modifier ($1.50) | 3 × ($10 + $1.50) | $34.50 |
The cheese cost is added to the unit price before multiplying by 3 — not added flat after.
Pizza with nested modifiers
| Line | Calculation | Subtotal |
|---|---|---|
| 1× Pizza ($12) + Stuffed Crust ($3) + Garlic Butter ($0.50) | 1 × ($12 + $3 + $0.50) | $15.50 |
Nested modifier costs from the Crust modifier group accumulate into the unit price before the outer quantity is applied.
Pasta platter with tiered pricing (3 dishes)
Using the tiered group from the example above (1st–2nd dish free, 3rd–4th at $8.00):
| Selection | k | Tier Price |
|---|---|---|
| 1st dish | 0 | $0.00 |
| 2nd dish | 1 | $0.00 |
| 3rd dish | 2 | $8.00 |
Subtotal = $0 + $0 + $8 = $8.00. Any nested modifiers on each dish are added to that dish's tier price, not to the parent item.
The Amounts block on a validated order (taxes, fees, promotions, total) remains server-authoritative and is returned by the validateOrder endpoint — the helper above only computes the cart subtotal you can show before validating.
Caching
CatalogSets are immutable — once created, a catalogSetId always returns the same data. Cache aggressively:
- Use
catalogSetIdas the cache key - The
getCatalogSetendpoint supports ETag caching — includeIf-None-Matchwith the ETag from a previous response to receive a304 Not Modifiedwhen the catalog hasn't changed - When a store updates its menu, a new CatalogSet is created with a new
catalogSetId
Related
- API Reference — Endpoint and schema documentation
- Order Lifecycle — How orders are validated against the catalog
- Schemas — Type definitions and data models