> ## Documentation Index
> Fetch the complete documentation index at: https://firespark.cloud/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Menu sync

> Push catalog data from your POS or RMS into Fire spark and compose menus per channel, store, fulfillment, and schedule.

Menu sync is how your POS or RMS becomes the **source of truth** for what customers can order on every connected channel. You push catalog entities once, then Fire spark **composes** what each surface shows — different assortments by channel, store, fulfillment type, or schedule without maintaining a separate integration per partner.

<Card title="New to Fire spark?" icon="lightbulb" href="/docs/integrations-api/introduction">
  Read the Integrations API introduction for authentication, webhooks, and the
  full integration model.
</Card>

## Where menu sync sits in the stack

Your operational system owns items, prices, and availability. Fire spark sits in the middle and publishes composed menus to owned channels and aggregators.

```mermaid theme={null}
flowchart LR
    subgraph POS["POS / RMS"]
        CAT["Catalog master\n(products, modifiers, prices)"]
    end

    subgraph FS["Fire spark"]
        INT["Integrations API"]
        COMPOSE["Menu composition\n(channel · store · fulfillment · schedule)"]
    end

    subgraph CH["Sales channels"]
        APP["App"]
        WEB["Web"]
        UE["Uber Eats"]
        OTHER["Rappi · PedidosYa · kiosk · …"]
    end

    CAT -->|"PUT /menus\nPUT /menus/products/{id}"| INT
    INT --> COMPOSE
    COMPOSE --> APP & WEB & UE & OTHER
```

When a customer orders, line item `id` values from the composed menu appear on the order payload and on the POS ticket after [`order.injected`](/docs/integrations-api/orders/webhooks). Keep external product ids stable across menu sync and order injection.

## Prerequisites

Create **stores**, **channels**, and **fulfillment types** in the [Fire spark dashboard](https://firespark.cloud) before your first menu upsert. Each resource uses the **same external `id`** you use in your POS or RMS.

<Steps>
  <Step title="Obtain an access token">
    Authenticate with OAuth 2.0 client credentials. Menu endpoints require
    `menus:read` and `menus:write`. See
    [Token](/docs/integrations-api/oauth/token/post).
  </Step>

  <Step title="Confirm fulfillment options">
    Verify delivery, pickup, dine-in, and any custom fulfillment types exist in
    the dashboard. See [List fulfillment](/docs/integrations-api/fulfillment/get).
  </Step>

  <Step title="Confirm stores">
    Verify each merchant location and its external ids. Stores carry channel
    availability and local rules. See [List
    stores](/docs/integrations-api/stores/get).
  </Step>

  <Step title="Confirm channels">
    Verify the channels configured for the merchant — app, web, kiosk,
    aggregators, and others. See [List
    channels](/docs/integrations-api/channels/get).
  </Step>
</Steps>

<Warning>
  Menu upserts return `409` when referenced `fulfillment_id`, `store_id`,
  `channel_id`, or `brand_id` values do not exist for the merchant. Create
  stores, channels, fulfillment, and brands in the dashboard before the first
  sync.
</Warning>

## How menu composition works

A **composed menu** is the catalog a customer sees for one selling context. Fire spark scopes each menu to a **store**, **channel**, and **fulfillment** triple. You can add **schedules** so the same triple shows different categories at breakfast vs lunch.

```mermaid theme={null}
flowchart TB
    MASTER["POS catalog\n(products · categories · modifier groups)"]
    MASTER --> M1["Menu: downtown · app · delivery"]
    MASTER --> M2["Menu: downtown · uber-eats · delivery"]
    MASTER --> M3["Menu: downtown · app · pickup"]
    MASTER --> M4["Menu: airport · app · delivery"]

    M1 --> S1["Weekday lunch schedule\n11:00–15:00"]
    M2 --> S2["All-day aggregator subset"]
```

| Dimension                            | What you control                         | Example                                                         |
| ------------------------------------ | ---------------------------------------- | --------------------------------------------------------------- |
| [Store](/docs/concepts/stores)            | Location-specific assortment and pricing | Airport store excludes dine-in-only items                       |
| [Channel](/docs/concepts/channels)        | Surface-specific catalog                 | Full menu on app; delivery-friendly subset on Uber Eats         |
| [Fulfillment](/docs/concepts/fulfillment) | Mode-specific items                      | Delivery menu hides large-format drinks unavailable for courier |
| Schedule                             | Time-of-day or day-of-week menus         | Breakfast categories until 11:00; dinner menu after 17:00       |

You update the POS once. Your integration decides how many composed menus to push and which categories and products each menu includes.

<Info>
  For background on why menus are composed per context, see
  [Menus](/docs/concepts/menus). For category-level availability by channel and
  store, see [Categories](/docs/concepts/categories).
</Info>

## Catalog building blocks

Every composed menu is built from four entity types you send in the same request.

| Entity             | Role                                                                         | Learn more                                                                     |
| ------------------ | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| **Product**        | Sellable item — burger, combo base, drink, modifier option                   | [Products](/docs/concepts/products)                                                 |
| **Category**       | Browse section that lists product ids                                        | [Categories](/docs/concepts/categories)                                             |
| **Modifier group** | Choice rules attached to a product                                           | [Products — modifier groups](/docs/concepts/products#modifier-groups-and-modifiers) |
| **Menu**           | Named catalog for one store + channel + fulfillment, with optional schedules | This guide                                                                     |

### Products

A product is anything that can appear on a ticket: a standalone burger, a combo, or a modifier option such as "Cola."

Send products in the top-level `products` array on [Upsert menus](/docs/integrations-api/menus/put). Each product needs a stable external `id` — the same value your kitchen expects on injected orders.

| Field                             | When it matters                                    |
| --------------------------------- | -------------------------------------------------- |
| `pricing.price`                   | List price for this menu context                   |
| `pricing.modifier_group_ids`      | Modifier groups the customer must or can configure |
| `bundled_items`                   | Products included in a combo by default            |
| `availability.status`             | `AVAILABLE`, `UNAVAILABLE`, or `OUT_OF_STOCK`      |
| `availability.out_of_stock_until` | Temporary 86 from your POS                         |

### Categories

Categories organize products for browsing. Each category lists `items` — products and nested categories visible in that section on this menu.

You can hide a category on one channel while keeping it on another by composing different menus — not by duplicating product masters in your POS.

#### Nested subcategories

Menu sync models subcategories as a **reference tree**, not with a `parent_id` field. Declare every category in the flat `categories[]` array and build the tree with `type: "CATEGORY"` entries in the parent's `items`.

```mermaid theme={null}
flowchart TB
    MENU["menus[].category_ids → food"]
    FOOD["categories: food"]
    BURGERS["categories: burgers"]
    SIDES["categories: sides"]
    MENU --> FOOD
    FOOD -->|"items[{type:CATEGORY}]"| BURGERS
    FOOD -->|"items[{type:CATEGORY}]"| SIDES
    BURGERS -->|"items[{type:PRODUCT}]"| BURGER["classic-burger"]
    SIDES -->|"items[{type:PRODUCT}]"| FRIES["fries"]
```

| Step | What to send                                                                                                 |
| ---- | ------------------------------------------------------------------------------------------------------------ |
| 1    | Declare **all** categories in `categories[]` — roots and children share the same array                       |
| 2    | In the parent category, list children with `items: [{ "type": "CATEGORY", "id": "burgers" }, ...]`           |
| 3    | In leaf categories, list products with `items: [{ "type": "PRODUCT", "id": "..." }]`                         |
| 4    | In the menu, reference **only the root** in `menus[].category_ids` — Fire spark resolves the tree from there |

**Display order.** Array order is display order:

| Level         | Field                                   | What it orders                      |
| ------------- | --------------------------------------- | ----------------------------------- |
| Menu sections | `menus[].category_ids`                  | Root categories visible on the menu |
| Subcategories | `parent.items` with `type: "CATEGORY"`  | Subcategories inside the parent     |
| Products      | `category.items` with `type: "PRODUCT"` | Products inside the category        |

You may mix `CATEGORY` and `PRODUCT` entries in the same `items` array; array order defines how they appear together in the UI.

**Recommended convention:** put products on **leaf** categories and use intermediate categories only for navigation. Validation does not require this, but it avoids screens where customers see subcategories and products at the same level unintentionally.

**Depth limit:** up to **3 levels** counting the root linked from the menu — root (1) → subcategory (2) → sub-subcategory (3). Deeper nesting fails validation.

See the full payload in [Nested subcategories](/docs/integrations-api/examples/menu-sync/nested-categories).

<Note>
  When reading a composed menu with [List menus](/docs/integrations-api/menus/get),
  `categories[]` includes only the root categories from `category_ids`. Child
  category ids appear in the parent's `items` with `type: "CATEGORY"` — to
  resolve each child's name and products, keep your sync payload or walk the
  tree from the root in your integration.
</Note>

### Modifier groups and modifiers

Customization is modeled in two layers:

| Layer              | What it is                                                    | Example                                     |
| ------------------ | ------------------------------------------------------------- | ------------------------------------------- |
| **Modifier group** | The question and its rules (min/max selections, display type) | "Choose a drink" — required, pick exactly 1 |
| **Modifier**       | One selectable answer inside the group                        | Cola, Sprite, still water                   |

```mermaid theme={null}
flowchart TB
    COMBO["Combo meal (product)"]
    COMBO --> G1["Modifier group: Choose a drink\nmin 1 · max 1 · LIST"]
    G1 --> M1["Cola (product)"]
    G1 --> M2["Sprite (product)"]
    G1 --> M3["Still water (product)"]
    COMBO --> G2["Modifier group: Add extras\nmin 0 · max 3 · GRID"]
    G2 --> M4["Extra cheese (product)"]
    G2 --> M5["Bacon (product)"]
```

**Modifiers are products.** Each entry in a group's `modifier_ids` array is the external `id` of another product in the same payload. That means:

* A drink sold standalone and as a combo modifier shares one product record.
* When you 86 still water in the POS, set `availability.status` to `OUT_OF_STOCK` on that product — the modifier disappears from every group that references it.
* Kitchen tickets use real product ids, not free-text notes.

Attach groups to a parent product through `pricing.modifier_group_ids`. Set selection rules on the group:

| Group field             | Purpose                                              |
| ----------------------- | ---------------------------------------------------- |
| `minimum_quantity`      | Required selections (for example 1 drink on a combo) |
| `maximum_quantity`      | Cap for optional add-ons                             |
| `display_type`          | `LIST` or `GRID` in customer UI                      |
| `charge_above_quantity` | Free selections before additional charges apply      |

Per-modifier price, default quantity, display type (`RADIO`, `CHECKBOX`, `QUANTITY`), and charge rules live on the modifier product under `pricing.modifiers` — one entry per `modifier_group_id` where that product appears as an option.

See [Modifier display types](/docs/integrations-api/guides/modifier-display-types) for the quantity rules that determine each `display_type`, validation errors, and examples.

<Tip>
  Use the same external ids in menu sync, order line items, and POS mapping.
  When [`order.injected`](/docs/integrations-api/orders/webhooks) fires, modifier
  lines reference these ids in the order payload.
</Tip>

## Defaults first, override where needed

Omitted scope means **any** (`__ANY__`). You can publish one baseline menu and add narrower upserts only where your operation differs — instead of one API call per store, channel, and schedule combination.

| What you omit                    | What Fire spark assumes                  |
| -------------------------------- | ---------------------------------------- |
| `store_id` query parameter       | Menu applies to **any store**            |
| `channel_id` query parameter     | Menu applies to **any channel**          |
| `fulfillment_id` query parameter | Menu applies to **any fulfillment type** |
| `schedules: null` on the menu    | Available **24 hours, every day**        |

Add `fulfillment_id`, `store_id`, `channel_id`, `brand_id`, or schedule entries only when a specific context needs something different from that baseline. More specific upserts apply to that context alone; they do not remove the default for contexts you left untouched.

```mermaid theme={null}
flowchart TB
    BASE["PUT /menus\n(no fulfillment_id · no store_id · no channel_id · no brand_id · schedules: null)\n→ merchant-wide default"]
    BASE --> O1["?channel_id=uber-eats\naggregator subset"]
    BASE --> O2["?store_id=airport\nregional assortment"]
    BASE --> O3["schedules on menu entry\nweekday lunch only"]
```

<Tip>
  A merchant-wide default is **optional but recommended** when the customer has
  not chosen a store or fulfillment type yet — for example pickup vs delivery
  while browsing before checkout. Fire spark does not require a baseline upsert
  before scoped overrides.
</Tip>

**Suggested sync order** (when you use a baseline):

1. Push a **merchant-wide default** — `PUT /menus` with no query parameters and set `"schedules": null`. Send your full product catalog.
2. Push **overrides** — repeat the upsert with `?store_id=…`, `?channel_id=…`, `?fulfillment_id=…`, or a `schedules` array only where the catalog, pricing, or hours diverge from the baseline.

You can also sync only scoped menus if every selling context you support is already known.

## Sync workflow

<Steps>
  <Step title="Choose the selling context">
    Decide whether this upsert is the **merchant-wide default** (omit scope
    query parameters) or a **targeted override** for one store, channel, brand,
    and/or fulfillment type. See [Upsert menus](/docs/integrations-api/menus/put) for
    query parameters.
  </Step>

  <Step title="Build the payload">
    Send shared catalog entities (`products`, `categories`, `modifier_groups`)
    plus up to 10 `menus` entries in one request. Each menu object references
    `category_ids` and a `fulfillment_id`.
  </Step>

  <Step title="Upsert the menu">
    Call `PUT /menus` for the baseline, or add `?store_id=…` and/or
    `?channel_id=…` for overrides. Use `"style": "DEFAULT"` for owned channels,
    or `"style": "UBER_EATS"` for Uber Eats Menu API v2 shape. See [Upsert
    menus](/docs/integrations-api/menus/put).
  </Step>

  <Step title="Verify the result">
    Read back composed menus with [List
    menus](/docs/integrations-api/menus/get?store_id=downtown\&channel_id=app). Pass
    the same `store_id` and `channel_id` you used on the upsert. Confirm ids
    match your POS mapping.
  </Step>

  <Step title="Push incremental changes">
    For single-item updates — price changes, 86s, channel-specific overrides —
    call [Update product](/docs/integrations-api/menus/products/\[id]/put) instead of
    resending the full menu.
  </Step>
</Steps>

## Multiple channels, stores, and fulfillment modes

Each composed menu is keyed by context:

```
fulfillment_id + store_id + channel_id + brand_id + menu.id
```

When `fulfillment_id`, `store_id`, `channel_id`, or `brand_id` is omitted, that dimension is **all**. Use that to your advantage: one baseline upsert with the full catalog, then overrides only where needed.

| Scenario                      | Approach                                                                                                                        |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| Same catalog everywhere       | Single `PUT /menus` with no query params; `schedules: null` for 24/7                                                            |
| One aggregator needs a subset | Baseline (optional), then `PUT /menus?channel_id=uber-eats` with different `category_ids` — or sync that channel only           |
| One store has regional items  | Baseline (optional), then `PUT /menus?store_id=airport` — or sync that store only                                               |
| Delivery vs pickup            | `PUT /menus?fulfillment_id=delivery` vs `?fulfillment_id=pickup` — or separate menu ids per fulfillment when assortments differ |
| Brand with multiple labels    | Pass `brand_id` query parameter to scope upserts per brand                                                                      |

```mermaid theme={null}
flowchart TB
    POS["Single POS catalog"]
    POS --> DEF["PUT /menus\n(default · any store · any channel)"]
    DEF --> A["?channel_id=uber-eats"]
    DEF --> B["?store_id=airport"]
    DEF --> C["?fulfillment_id=pickup"]
```

### Item-level overrides across contexts

When one product needs different pricing or availability per channel, store, or fulfillment type without replacing whole menus, call [Update product](/docs/integrations-api/menus/products/\[id]/put) with `?store_id=…`, `?channel_id=…`, `?fulfillment_id=…`, and/or `?menu_id=…` when several menus share the same scope. The body uses the same menu product shape as [Upsert menus](/docs/integrations-api/menus/put) — top-level `pricing` and `availability`, not a nested `channels` object.

This matches how commercial teams think: push a baseline product once, then send a scoped update so Cola is `$0` on the combo in the app at downtown, while the default catalog keeps a different price elsewhere.

Edits made from the dashboard or using our sync API persist across normal upserts. To override these changes, send `x-force: true` on [Upsert menus](/docs/integrations-api/menus/put). To stage a sync without publishing yet, send `x-publish: false` and publish from the dashboard when ready.

## Schedules

Schedules restrict when a menu or individual entity is orderable.

| Level              | Field                                                                                                                                                      | Format                                                                                                                                                           |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Menu               | `menus[].schedules`                                                                                                                                        | Weekly array (`monday`–`sunday`); each period may include `start_time` / `end_time`, optional `closed` (default `false`), and optional `start_date` / `end_date` |
| Product / category | `availability.schedules`                                                                                                                                   | Same shape; applies within the composed menu                                                                                                                     |
| Product override   | `availability.schedules` on [Update product](/docs/integrations-api/menus/products/\[id]/put) with `?store_id` / `?channel_id` / `?fulfillment_id` / `?menu_id` | Per-context hours for one selling context or one composed menu                                                                                                   |

Use `null` or omit `schedules` when the menu is available around the clock — Fire spark treats that as **24 hours, every day**. Set a `schedules` array only when orderability is limited to specific days or time windows.

When you send a `schedules` array, Fire spark validates the full weekly shape:

| Rule            | Requirement                                                                                                                                                                    |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Week coverage   | Exactly **7** entries — one per weekday from `monday` through `sunday`                                                                                                         |
| Unique days     | Each `day_of_week` appears **once**                                                                                                                                            |
| Periods per day | At most **10** periods                                                                                                                                                         |
| Period fields   | Open periods require `start_time` and `end_time`. Optional `closed` defaults to `false`. `start_date` / `end_date` are nullable (both set or both `null`)                      |
| No overlap      | Null-date periods on the same day must not overlap in time. Specific-dated closed and open periods must not share a date range. Null-date + specific-dated periods may coexist |

Times in `start_time` and `end_time` use ISO time strings (for example `11:00:00`). Set `closed: true` when the period is a closed window (`start_time` / `end_time` may be omitted). Use `null` dates for the always-on fallback; set `start_date` / `end_date` for a more specific window. An empty `periods` array means closed all day.

Multiple menus in the same sync may share overlapping schedule windows. [Get menu](/docs/storefront-api/menus/get) returns every composed menu whose schedule matches the current time in `data`. When none are in schedule, it returns the menu or menus whose next window starts soonest. Menus with `schedules: null` are always considered active.

**Common patterns:**

* **Always-on default** — Omit schedules (`null`) on the baseline menu; add scheduled overrides only for dayparts that differ.
* **Daypart menus** — Separate menu ids for breakfast and lunch on the same store + channel + fulfillment, each with its own schedule and `category_ids`.
* **Closed windows** — Keep open hours on a day and add a `closed: true` period (optionally with `start_date` / `end_date`) for a holiday or maintenance block.
* **Seasonal categories** — Keep the menu always active; schedule the category or product `availability` instead.
* **Temporary 86** — Prefer `availability.status: OUT_OF_STOCK` with `out_of_stock_until` for item-level pauses without rebuilding schedules.

## Validation rules

Fire spark validates the full [Upsert menus](/docs/integrations-api/menus/put) payload before queuing a sync. These rules apply to the `DEFAULT` style.

### Products

| Field                                   | Rule                                                                                      |
| --------------------------------------- | ----------------------------------------------------------------------------------------- |
| `products`                              | At least 1 entry. At most 1000.                                                           |
| `id`, `name`, `pricing`, `availability` | Required on each product                                                                  |
| `status`                                | **Not accepted** on menu sync products. Use `availability.status` instead.                |
| `availability.status`                   | `AVAILABLE`, `UNAVAILABLE`, or `OUT_OF_STOCK`                                             |
| `availability.schedules`                | Same weekly rules as menu schedules when provided. Omit or set `null` for no restriction. |
| `availability.out_of_stock_until`       | Optional ISO 8601 datetime                                                                |
| `labels`                                | Optional. At most 64 strings (1–128 characters each). Defaults to `[]`.                   |
| `tags`                                  | Optional. Object of boolean, string, or number values. Defaults to `{}`.                  |

### Categories

| Field                                 | Rule                                                                                                                                                                        |
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `categories`                          | At least 1 entry. At most 100.                                                                                                                                              |
| `id`, `name`, `items`, `availability` | Required on each category                                                                                                                                                   |
| `status`                              | **Not accepted** on menu sync categories. Use `availability.status` instead.                                                                                                |
| `items`                               | At least **1** entry. At most 1000. Each entry requires `type` (`CATEGORY` or `PRODUCT`) and `id`. Array order is display order. No duplicate ids within the same category. |
| `availability.status`                 | `ACTIVE`, `INACTIVE`, or `HIDDEN`                                                                                                                                           |
| `availability.schedules`              | Same weekly rules as menu schedules when provided. Omit or set `null` for no restriction.                                                                                   |
| `availability.inactive_until`         | Optional ISO 8601 datetime                                                                                                                                                  |
| `labels`                              | Optional. At most 64 strings (1–128 characters each). Defaults to `[]`.                                                                                                     |
| `tags`                                | Optional. Object of boolean, string, or number values. Defaults to `{}`.                                                                                                    |

**Nesting** (see [Nested subcategories](#nested-subcategories)):

| Rule         | Requirement                                                                                                           |
| ------------ | --------------------------------------------------------------------------------------------------------------------- |
| References   | Each `items[].id` must exist in `products[]` (when `type` is `PRODUCT`) or `categories[]` (when `type` is `CATEGORY`) |
| Reachability | Every category must appear in `menus[].category_ids` or be referenced as a child in another category's `items`        |
| Depth        | At most **3** levels from each root in `category_ids`, counting the root as level 1                                   |
| Cycles       | A category cannot be an ancestor of itself                                                                            |

### Menu definitions

| Field                                  | Rule                                                                                                               |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `menus`                                | At least 1 entry. At most 10.                                                                                      |
| `id`, `name`, `category_ids`, `status` | Required on each menu                                                                                              |
| `category_ids`                         | At least **1** category id. At most 100. **Root** categories only. Array order is display order. No duplicate ids. |
| `status`                               | `ACTIVE` or `INACTIVE`                                                                                             |
| `schedules`                            | Optional. `null` or omitted for 24/7. When set, must pass the weekly schedule rules above.                         |

### Modifier groups

| Field             | Rule                                  |
| ----------------- | ------------------------------------- |
| `modifier_groups` | Optional. At most 1000 when provided. |
| `modifier_ids`    | At least **2** entries per group      |

### Recommendations

| Field                                        | Rule                                                                                                                       |
| -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `recommendations`                            | Optional. At most 1000 when provided. Defaults to `[]`.                                                                    |
| `id`, `title`, `type`, `target_product_id`   | Required on each recommendation                                                                                            |
| `type`                                       | `UPSELL`, `CROSS_SELL`, or `DOWNSELL`                                                                                      |
| `target_product_id`                          | Must reference a product `id` in `products`                                                                                |
| `pricing.recommendations_ids`                | Optional on each product. Each id must exist in `recommendations`                                                          |
| `pricing.recommendations`                    | Optional on each product. Each `recommendation_id` must exist in `recommendations`. Duplicate ids are rejected per product |
| `pricing.recommendations[].default_quantity` | Optional. Defaults to `0`. Independent from `pricing.modifiers[].default_quantity`                                         |

When a sync defines multiple menus, Fire spark composes each menu from the shared catalog and keeps only recommendations whose `target_product_id` is reachable in that menu — the same composition rules as `products` and `modifier_groups`.

### Labels and tags

Products and categories accept optional `labels` and `tags`. Omit either field on existing syncs — Fire spark defaults to `[]` and `{}` and does not reject payloads that leave them out.

| Field    | Shape                                            | Use                                                                                             |
| -------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
| `labels` | `string[]`                                       | Filters and quick selection in the Dashboard.                                                   |
| `tags`   | `{ [key: string]: boolean \| string \| number }` | Advanced cases such as custom rules. See [Custom rules](/docs/integrations-api/guides/custom-rules). |

```json theme={null}
{
  "id": "burger-classic",
  "labels": ["featured", "lunch"],
  "tags": { "promo_eligible": true, "kitchen_station": "grill" }
}
```

<Warning>
  Category availability uses `ACTIVE`, `INACTIVE`, and `HIDDEN` — not the
  product values `AVAILABLE`, `UNAVAILABLE`, and `OUT_OF_STOCK`. Categories also
  use `inactive_until`, not `out_of_stock_until`.
</Warning>

## Sync styles

[Upsert menus](/docs/integrations-api/menus/put) accepts a discriminated union on `style`:

<Tabs>
  <Tab title="DEFAULT">
    Fire spark native format for owned channels (app, web, kiosk, call center).
    Use field names `products`, `categories`, `modifier_groups`, and localized
    `name` objects keyed by locale (for example `en_us`).

    Best when you control the customer UI through the
    [Storefront API](/docs/storefront-api/introduction) or a custom front end.
  </Tab>

  <Tab title="UBER_EATS">
    Uber Eats Menu API v2 payload. Products and modifiers share the `items`
    array; prices are integer cents in `price_info.price`.

    Best when pushing directly to an Uber Eats aggregator channel without
    mapping fields yourself. See Uber's
    [example menu payloads](https://developer.uber.com/docs/eats/references/api/v2/example-menu-payloads)
    for nested modifiers and bundled items.
  </Tab>
</Tabs>

## Full sync vs incremental updates

| Method           | Endpoint                                                     | When to use                                                                                       |
| ---------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- |
| Full menu upsert | [Upsert menus](/docs/integrations-api/menus/put)                  | Initial load, structural changes (new categories, modifier groups), or recomposing `category_ids` |
| Single product   | [Update product](/docs/integrations-api/menus/products/\[id]/put) | Price change, 86, or per-channel override on one item                                             |
| Read back        | [List menus](/docs/integrations-api/menus/get)                    | Reconciliation and mapping verification for a store and channel (`?store_id` + `?channel_id`)     |

<Warning>
  Menu `id` values are immutable after creation. Send the same external menu
  `id` on every sync for a given selling context.
</Warning>

## Map ids back to your POS

| Fire spark field                                       | Map to                                                |
| ------------------------------------------------------ | ----------------------------------------------------- |
| `products[].id`                                        | POS item / PLU / SKU                                  |
| `categories[].id`                                      | POS menu section or category code                     |
| `menus[].id`                                           | POS menu version or daypart identifier                |
| `fulfillment_id`, `store_id`, `channel_id`, `brand_id` | Location, sales surface, and service mode in your RMS |

Use external ids for cross-system mapping. Use `*_uid` fields only when referencing Fire spark resources in other API calls.

## Related endpoints

<CardGroup cols={2}>
  <Card title="Upsert menus" icon="upload" href="/docs/integrations-api/menus/put">
    Create or update composed menus
  </Card>

  <Card title="List menus" icon="list" href="/docs/integrations-api/menus/get">
    Read menus for reconciliation
  </Card>

  <Card title="Update product" icon="pen" href="/docs/integrations-api/menus/products/[id]/put">
    Push item-level changes
  </Card>

  <Card title="Order webhooks" icon="webhook" href="/docs/integrations-api/orders/webhooks">
    Receive orders with the same product ids
  </Card>

  <Card title="List stores" icon="store" href="/docs/integrations-api/stores/get">
    Read store external ids before menus
  </Card>

  <Card title="List fulfillment" icon="truck" href="/docs/integrations-api/fulfillment/get">
    Read fulfillment external ids before menus
  </Card>
</CardGroup>

## Example payloads

Copy-paste payloads for common menu sync scenarios:

<CardGroup cols={2}>
  <Card title="Simple menu" icon="utensils" href="/docs/integrations-api/examples/menu-sync/simple-menu">
    3 products, 1 category, 1 modifier group
  </Card>

  <Card title="Nested categories" icon="list-tree" href="/docs/integrations-api/examples/menu-sync/nested-categories">
    Root category with two subcategories
  </Card>

  <Card title="Multi-schedule" icon="clock" href="/docs/integrations-api/examples/menu-sync/multi-schedule">
    2 daypart menus, no modifier groups
  </Card>

  <Card title="Multi-level modifiers" icon="list-tree" href="/docs/integrations-api/examples/menu-sync/multi-level-modifiers">
    3 nested modifier group levels
  </Card>

  <Card title="Modifier overrides" icon="sliders" href="/docs/integrations-api/examples/menu-sync/modifier-group-overrides">
    Price, min, and max overrides
  </Card>

  <Card title="Product schedule" icon="calendar" href="/docs/integrations-api/examples/menu-sync/product-special-schedule">
    Happy-hour availability on one product
  </Card>

  <Card title="Product recommendations" icon="sparkles" href="/docs/integrations-api/examples/menu-sync/product-recommendations">
    Combo upsell, side cross-sell, and combo downsell
  </Card>
</CardGroup>
