Gett Developer Portal
  • Welcome
  • Distribution Partners
  • Brand Partners
  • Commerce Partners
  • Ecosystem Partners
  • Errors
  • API Reference
Documentation
  • Get Started
  • Marketfront SDK
  • API Reference
Resources
  • Payments
Company
  • Gett
  • Terms of Service
  • Privacy Policy

Copyright 2026 Gett. All rights reserved.

Marketfront SDK
Marketfront API
    Getting StartedConventionsOrder LifecycleCatalogSet
    API Reference
Marketfront AI
Shared Guides
powered by Zuplo
Marketfront API

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
catalogs[catalogId] → sectionIds → sections[sectionId] → sectionIds → sections[sectionId] (nested sub-sections) → itemIds → items[itemId] → modifierGroupIds → modifierGroups[modifierGroupId] → itemIds → items[itemId] (modifiers are items!)

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:

FieldTypeDescription
dayOfWeekstring"Sunday" through "Saturday"
startstringStart time in HH:mm:ss format
endstringEnd 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:00 end time = end of day (not midnight-to-midnight)
  • Overnight windows must be split into two entries:
Code
// Friday 10 PM - Saturday 2 AM: [ { "dayOfWeek": "Friday", "start": "22:00:00", "end": "00:00:00" }, { "dayOfWeek": "Saturday", "start": "00:00:00", "end": "02:00:00" } ]

Sections

Sections organize items into categories (e.g., "Appetizers", "Entrees") and support nesting.

FieldDescription
itemIdsOrdered list of item IDs in this section
sectionIdsChild 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 itemIds and sectionIds is 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.

FieldDescription
basePricePrice before modifiers. For modifier options, this is the upcharge (e.g., Large = +$1.00)
startingAtHeadline "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)
compareAtOriginal/marketplace price for showing savings (strikethrough pricing)
isAvailableWhether item can currently be ordered
modifierGroupIdsCustomization 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:

MinMaxMeaningExample
01Optional single choice"Add a side?"
11Required single choice (radio)"Choose size: S / M / L"
0NOptional, up to N"Add up to 5 toppings"
1NRequired, 1 to N"Pick 1-3 sauces"
NNExactly N required"Pick 2 sides"

Validation rules:

  • minimumAllowed >= 0
  • maximumAllowed >= minimumAllowed
  • When enableDuplicateItems is 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
"Baker's Dozen" (13 bagels): ModifierGroup: "Choose Your Bagels" Items: [Plain, Sesame, Everything, Cinnamon Raisin] minimumAllowed: 13 maximumAllowed: 13 enableDuplicateItems: true Valid selection: 5× Plain, 4× Everything, 4× Cinnamon Raisin = 13

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
{ "defaultItems": [ { "itemId": "lettuce-id", "quantity": 1 }, { "itemId": "tomato-id", "quantity": 1 } ] }

Rules:

  • Each itemId must exist in the modifier group's itemIds
  • 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
Pizza → "Crust" modifier group → "Stuffed Crust" item ($3.00) → "Choose Stuffing" modifier group → "Cheese" item ($0.00) → "Garlic Butter" item ($0.50)

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
Milkshake (parent) basePrice: $0.00 ← parent contributes nothing startingAt: $8.00 ← display: "from $8.00" (cheapest variant) → "Size" modifier group (minimumAllowed = 1, maximumAllowed = 1) → "Small Milkshake" basePrice: $8.00 (absolute) → "Medium Milkshake" basePrice: $9.00 (absolute) → "Large Milkshake" basePrice: $10.00 (absolute)

Conventions:

  • parent.basePrice = 0. The parent contributes nothing to the cart total — the absolute price comes entirely from the selected variant.
  • parent.startingAt carries 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 minimumAllowed and maximumAllowed are both 1 (radio-style required pick).
  • The variant child is a regular Item and may carry its own nested modifierGroupIds (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
{ "name": "Select Pasta Dishes", "minimumAllowed": 2, "maximumAllowed": 6, "enableDuplicateItems": true, "tieredPricing": [ { "offset": 0, "price": 0.00 }, { "offset": 2, "price": 8.00 }, { "offset": 4, "price": 7.00 } ] }

How to resolve the tier for the k-th selection (0-based): use the tier with the greatest offset that is ≤ k.

SelectionkApplicable TierPrice
1st dish0offset 0$0.00
2nd dish1offset 0$0.00
3rd dish2offset 2$8.00
4th dish3offset 2$8.00
5th dish4offset 4$7.00
6th dish5offset 4$7.00

Tier rules:

  • Offsets must be non-negative and unique
  • Tiers must be in ascending offset order
  • When tieredPricing is null or empty, fall back to each item's basePrice

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. basePrice plus 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 as startingAt + modifiers — startingAt already folds in the cheapest required modifier, so adding modifiers on top double-counts.

Worked example — front-loaded "sundae" (parent basePrice $0, startingAt $10):

Code
Sundae (parent) basePrice: $0.00 startingAt: $10.00 ← headline "from $10.00" → "Size" group (required, min = max = 1) → Small basePrice: $10.00 (display delta: $0 — the cheapest, absorbed into the headline) → Medium basePrice: $12.00 (display delta: +$2) → Large basePrice: $14.00 (display delta: +$4)
User picksRunning 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
export function lineItemPrice(li: LineItem, catalogSet: CatalogSet): number { const item = lookupItem(catalogSet, li.itemId); let unitPrice = item.basePrice ?? 0; for (const mg of li.modifierGroups ?? []) { unitPrice += modifierGroupPrice(mg, catalogSet); } return unitPrice * li.quantity; } function modifierGroupPrice(mg: CartModifierGroup, catalogSet: CatalogSet): number { const groupDef = lookupModifierGroup(catalogSet, mg.modifierGroupId); const selections = mg.lineItems ?? []; let total = 0; if (groupDef.tieredPricing && groupDef.tieredPricing.length > 0) { const tiers = sortedTiers(groupDef.tieredPricing); let k = 0; for (const sel of selections) { let nestedCost = 0; for (const nestedMg of sel.modifierGroups ?? []) { nestedCost += modifierGroupPrice(nestedMg, catalogSet); } for (let i = 0; i < sel.quantity; i++) { total += tierPriceAt(tiers, k) + nestedCost; k++; } } } else { for (const sel of selections) { const selItem = lookupItem(catalogSet, sel.itemId); let unitPrice = selItem.basePrice ?? 0; for (const nestedMg of sel.modifierGroups ?? []) { unitPrice += modifierGroupPrice(nestedMg, catalogSet); } total += unitPrice * sel.quantity; } } return total; }

tierPriceAt(tiers, k) returns the tier with the greatest offset that is <= k.

Worked Examples

Burger with cheese (qty > 1)

LineCalculationSubtotal
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

LineCalculationSubtotal
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):

SelectionkTier Price
1st dish0$0.00
2nd dish1$0.00
3rd dish2$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 catalogSetId as the cache key
  • The getCatalogSet endpoint supports ETag caching — include If-None-Match with the ETag from a previous response to receive a 304 Not Modified when 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
Order LifecycleMarketfront AI
On this page
  • Structure Overview
  • Navigating the Graph
  • Catalogs & Availability
  • Sections
  • Items
  • Modifier Groups
    • Selection Rules
    • Duplicate Selections
    • Default Items
    • Nested Modifiers
    • Variant Size Groups
  • Tiered Pricing
  • Price Calculation
    • Headline "from" price vs running total
    • Worked Examples
  • Caching
  • Related
JSON
JSON
JSON
TypeScript