> ## 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.

# Sincronización de menús

> Envía el catálogo desde tu POS o RMS a Fire spark y compón menús por canal, tienda, fulfillment y horario.

La sincronización de menús es cómo tu POS o RMS se convierte en la **fuente de verdad** de lo que el cliente puede pedir en cada canal conectado. Envías las entidades del catálogo una vez y Fire spark **compone** lo que ve cada superficie — distintos surtidos por canal, tienda, tipo de fulfillment u horario, sin mantener una integración separada por partner.

> [card] [¿Nuevo en Fire spark?](https://firespark.cloud/docs/es/integrations-api/introduction.md)
>
> Lee la introducción a la API de integraciones para autenticación, webhooks y
> el modelo completo de integración.

## Dónde encaja la sincronización de menús

Tu sistema operativo es dueño de ítems, precios y disponibilidad. Fire spark queda en el medio y publica menús compuestos hacia canales propios y agregadores.

```mermaid theme={null}
flowchart LR
    subgraph POS["POS / RMS"]
        CAT["Catálogo maestro\n(productos, modificadores, precios)"]
    end

    subgraph FS["Fire spark"]
        INT["API de integraciones"]
        COMPOSE["Composición de menús\n(canal · tienda · fulfillment · horario)"]
    end

    subgraph CH["Canales de venta"]
        APP["App"]
        WEB["Web"]
        UE["Uber Eats"]
        OTHER["Rappi · PedidosYa · kiosco · …"]
    end

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

Cuando un cliente pide, los `id` de línea del menú compuesto aparecen en el payload del pedido y en el ticket del POS tras [`order.status.changed`](https://firespark.cloud/docs/es/integrations-api/orders/webhooks). Mantén los ids externos de producto estables entre la sync de menú y el traspaso a cocina.

## Prerrequisitos

Crea **tiendas**, **canales** y **tipos de fulfillment** en el [dashboard de Fire spark](https://firespark.cloud) antes del primer upsert de menú. Cada recurso usa el **mismo `id` externo** que empleas en tu POS o RMS.

**1. Obtén un access token**

Autentica con OAuth 2.0 client credentials. Los endpoints de menú requieren
`menus:read` y `menus:write`. Ver
[Token](https://firespark.cloud/docs/es/integrations-api/oauth/token/post.md).

**2. Confirma opciones de fulfillment**

Verifica que delivery, pickup, dine-in y cualquier tipo custom existan en el
dashboard. Ver [List fulfillment](https://firespark.cloud/docs/es/integrations-api/fulfillment/get.md).

**3. Confirma tiendas**

Verifica cada ubicación del comercio y sus ids externos. Las tiendas llevan
disponibilidad por canal y reglas locales. Ver [List
stores](https://firespark.cloud/docs/es/integrations-api/stores/get.md).

**4. Confirma canales**

Verifica los canales configurados — app, web, kiosco, agregadores y más. Ver
[List channels](https://firespark.cloud/docs/es/integrations-api/channels/get.md).

> [!WARNING]
>
> Los upserts de menú devuelven `409` cuando `fulfillment_id`, `store_id`,
> `channel_id` o `brand_id` referenciados no existen para el comercio. Crea
> tiendas, canales, fulfillment y marcas en el dashboard antes de la primera
> sync.

## Cómo funciona la composición de menús

Un **menú compuesto** es el catálogo que ve el cliente para un contexto de venta. Fire spark acota cada menú a una tripleta **tienda + canal + fulfillment**. Puedes añadir **horarios** para que la misma tripleta muestre categorías distintas en desayuno vs almuerzo.

```mermaid theme={null}
flowchart TB
    MASTER["Catálogo POS\n(productos · categorías · grupos de modificadores)"]
    MASTER --> M1["Menú: downtown · app · delivery"]
    MASTER --> M2["Menú: downtown · uber-eats · delivery"]
    MASTER --> M3["Menú: downtown · app · pickup"]
    MASTER --> M4["Menú: airport · app · delivery"]

    M1 --> S1["Horario almuerzo entre semana\n11:00–15:00"]
    M2 --> S2["Subset agregador todo el día"]
```

| Dimensión | Qué controlas | Ejemplo |
| - | - | - |
| [Tienda](https://firespark.cloud/docs/es/concepts/stores.md) | Surtido y precios por ubicación | La tienda aeropuerto excluye ítems solo dine-in |
| [Canal](https://firespark.cloud/docs/es/concepts/channels.md) | Catálogo por superficie | Menú completo en app; subset delivery-friendly en Uber Eats |
| [Fulfillment](https://firespark.cloud/docs/es/concepts/fulfillment.md) | Ítems por modo de servicio | Menú delivery oculta bebidas grandes no disponibles para courier |
| Horario | Menús por franja horaria o día | Categorías desayuno hasta las 11:00; menú cena después de las 17:00 |

Actualizas el POS una vez. Tu integración decide cuántos menús compuestos enviar y qué categorías e ítems incluye cada uno.

> [!NOTE]
>
> Para el contexto de por qué los menús se componen por contexto, ver
> [Menús](https://firespark.cloud/docs/es/concepts/menus.md). Para disponibilidad de categorías por canal y
> tienda, ver [Categorías](https://firespark.cloud/docs/es/concepts/categories.md).

## Bloques del catálogo

Cada menú compuesto se arma con cuatro tipos de entidad que envías en la misma solicitud.

| Entidad | Rol | Más información |
| - | - | - |
| **Producto** | Ítem vendible — burger, base de combo, bebida, opción modificadora | [Productos](https://firespark.cloud/docs/es/concepts/products.md) |
| **Categoría** | Sección de navegación que lista ids de producto | [Categorías](https://firespark.cloud/docs/es/concepts/categories.md) |
| **Grupo de modificadores** | Reglas de elección ligadas a un producto | [Productos — grupos de modificadores](https://firespark.cloud/docs/es/concepts/products.md#grupos-de-modificadores-y-modificadores) |
| **Menú** | Catálogo nombrado para una tienda + canal + fulfillment, con horarios opcionales | Esta guía |

### Productos

Un producto es cualquier cosa que puede salir en un ticket: un burger suelto, un combo o una opción modificadora como "Cola".

Envía productos en el array `products` de nivel superior en [Upsert menus](https://firespark.cloud/docs/es/integrations-api/menus/put.md). Cada producto necesita un `id` externo estable — el mismo valor que tu cocina espera en pedidos inyectados.

| Campo | Cuándo importa |
| - | - |
| `pricing.price` | Precio de lista para este contexto de menú |
| `pricing.modifier_group_ids` | Grupos que el cliente debe o puede configurar |
| `bundled_items` | Productos incluidos en un combo por defecto |
| `availability.status` | `AVAILABLE`, `HIDDEN` u `OUT_OF_STOCK` |
| `availability.out_of_stock_until` | 86 temporal desde tu POS |

### Categorías

Las categorías organizan productos para navegar. Cada categoría lista `items` — productos y categorías anidadas visibles en esa sección de este menú.

Puedes ocultar una categoría en un canal y mantenerla en otro componiendo menús distintos — sin duplicar maestros de ítem en el POS.

#### Subcategorías anidadas

El payload de menu sync modela subcategorías como un **árbol por referencias**, no con un campo `parent_id`. Declara todas las categorías en el array plano `categories[]` y arma el árbol con entradas `type: "CATEGORY"` en `items` del padre.

```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"]
```

| Paso | Qué enviar |
| - | - |
| 1 | Declara **todas** las categorías en `categories[]` — raíz e hijas comparten el mismo array |
| 2 | En la categoría padre, lista hijas con `items: [{ "type": "CATEGORY", "id": "burgers" }, ...]` |
| 3 | En las categorías hoja, lista productos con `items: [{ "type": "PRODUCT", "id": "..." }]` |
| 4 | En el menú, referencia **solo la raíz** en `menus[].category_ids` — Fire spark resuelve el árbol desde ahí |

**Orden de display.** El orden en cada array es el orden de display:

| Nivel | Campo | Qué ordena |
| - | - | - |
| Secciones del menú | `menus[].category_ids` | Categorías raíz visibles en el menú |
| Subcategorías | `parent.items` con `type: "CATEGORY"` | Subcategorías dentro del padre |
| Productos | `category.items` con `type: "PRODUCT"` | Productos dentro de la categoría |

Puedes mezclar entradas `CATEGORY` y `PRODUCT` en el mismo `items`; el orden del array define cómo aparecen juntas en la UI.

**Convención recomendada:** coloca productos en categorías **hoja** y usa categorías intermedias solo para navegar. La validación no lo exige, pero evita pantallas donde el cliente ve subcategorías y productos al mismo nivel sin quererlo.

**Límite de profundidad:** hasta **3 niveles** contando la raíz vinculada al menú — raíz (1) → subcategoría (2) → sub-subcategoría (3). Más profundo falla la validación.

Ver el payload completo en [Subcategorías anidadas](https://firespark.cloud/docs/es/integrations-api/examples/menu-sync/nested-categories.md).

> [!NOTE]
>
> Al leer un menú compuesto con [List menus](https://firespark.cloud/docs/es/integrations-api/menus/get.md),
> `categories[]` incluye solo las categorías raíz de `category_ids`. Los ids de
> subcategoría aparecen en `items` del padre con `type: "CATEGORY"` — para
> resolver nombre y productos de cada hija, conserva el payload de sync o navega
> el árbol desde la raíz en tu integración.

### Grupos de modificadores y modificadores

La personalización se modela en dos capas:

| Capa | Qué es | Ejemplo |
| - | - | - |
| **Grupo de modificadores** | La pregunta y sus reglas (min/máx selecciones, tipo de display) | "Elige una bebida" — obligatorio, elige exactamente 1 |
| **Modificador** | Una respuesta seleccionable dentro del grupo | Cola, Sprite, agua sin gas |

```mermaid theme={null}
flowchart TB
    COMBO["Combo meal (producto)"]
    COMBO --> G1["Grupo: Elige bebida\nmin 1 · max 1 · LIST"]
    G1 --> M1["Cola (producto)"]
    G1 --> M2["Sprite (producto)"]
    G1 --> M3["Agua sin gas (producto)"]
    COMBO --> G2["Grupo: Extras\nmin 0 · max 3 · GRID"]
    G2 --> M4["Queso extra (producto)"]
    G2 --> M5["Tocino (producto)"]
```

**Los modificadores son productos.** Cada entrada en `modifier_ids` del grupo es el `id` externo de otro producto en el mismo payload. Eso implica:

* Una bebida vendida suelta y como modificador de combo comparte un registro de producto.
* Cuando haces 86 de agua sin gas en el POS, pon `availability.status` en `OUT_OF_STOCK` en ese producto — el modificador desaparece de todos los grupos que lo referencian.
* Los tickets de cocina usan ids de producto reales, no notas en texto libre.

Liga grupos al producto padre con `pricing.modifier_group_ids`. Define reglas de selección en el grupo:

| Campo del grupo | Propósito |
| - | - |
| `minimum_quantity` | Selecciones obligatorias (por ejemplo 1 bebida en un combo) |
| `maximum_quantity` | Tope para extras opcionales |
| `display_type` | `LIST` o `GRID` en la UI del cliente |
| `charge_above_quantity` | Selecciones gratis antes de cobrar adicionales |

Precio por modificador, cantidad por defecto, tipo de display (`RADIO`, `CHECKBOX`, `QUANTITY`) y reglas de cobro viven en el producto modificador bajo `pricing.modifiers` — una entrada por cada `modifier_group_id` donde ese producto aparece como opción. `image_url` en esa entrada es opcional y reemplaza la imagen del producto solo dentro de ese grupo. Si la omites, se usa la imagen del producto.

Las imágenes de productos, categorías, variantes y modificadores se copian a almacenamiento público (`/storage/v1/object/public/multimedia/...`). Las respuestas sirven `/storage/v1/render/image/public/...?quality=80`. Agrega `width`, `height` y `resize` (`cover`, `contain` o `fill`) en esa URL cuando necesites un tamaño concreto.

Consulta [Tipos de display de modificadores](https://firespark.cloud/docs/es/integrations-api/guides/modifier-display-types.md) para las reglas de cantidad que determinan cada `display_type`, los errores de validación y ejemplos.

> [!TIP]
>
> Usa los mismos ids externos en sync de menú, líneas de pedido y mapeo POS.
> Cuando llega [`order.status.changed`](https://firespark.cloud/docs/es/integrations-api/orders/webhooks),
> las líneas modificadoras referencian esos ids en el payload.

## Defaults primero, override solo donde haga falta

Si omites el alcance, Fire spark asume **cualquiera**. Puedes publicar un menú base y añadir upserts más acotados solo donde tu operación difiere — en lugar de una llamada API por cada combinación de tienda, canal y horario.

| Lo que omites | Qué asume Fire spark |
| - | - |
| Parámetro `store_id` en la URL | El menú aplica a **cualquier tienda** |
| Parámetro `channel_id` en la URL | El menú aplica a **cualquier canal** |
| `fulfillment_id` en la entrada de menú | El menú aplica a **cualquier tipo de fulfillment** |
| `schedules: null` en el menú | Disponible **24 horas, todos los días** |

Añade `fulfillment_id`, `store_id`, `channel_id`, `brand_id` o entradas de horario solo cuando un contexto concreto necesite algo distinto al baseline. Los upserts más específicos aplican solo a ese contexto; no eliminan el default en los contextos que no tocaste.

```mermaid theme={null}
flowchart TB
    BASE["PUT /menus\n(sin fulfillment_id · sin store_id · sin channel_id · sin brand_id · schedules: null)\n→ default para todo el comercio"]
    BASE --> O1["?channel_id=uber-eats\nsubset agregador"]
    BASE --> O2["?store_id=airport\nsurtido regional"]
    BASE --> O3["schedules en la entrada de menú\nsolo almuerzo entre semana"]
```

> [!TIP]
>
> Un default a nivel comercio es **opcional pero recomendado** cuando el cliente
> aún no eligió tienda o tipo de fulfillment — por ejemplo pickup vs delivery
> mientras navega antes del checkout. Fire spark no exige un upsert baseline
> antes de overrides acotados.

**Orden sugerido de sync** (si usas un baseline):

1. Envía un **default a nivel comercio** — `PUT /menus` sin query params, omite `fulfillment_id` en cada entrada de menú y usa `"schedules": null`. Incluye el catálogo completo de productos.
2. Envía **overrides** — repite el upsert con `?store_id=…`, `?channel_id=…`, `fulfillment_id` o un array `schedules` solo donde el catálogo, precio u horarios difieran del baseline.

También puedes sincronizar solo menús acotados si ya conoces cada contexto de venta que vas a servir.

## Flujo de sincronización

**1. Elige el contexto de venta**

Decide si este upsert es el **default del comercio** (omite `store_id`,
`channel_id` y `fulfillment_id`) o un **override acotado** para una tienda,
canal y/o tipo de fulfillment. Ver [Upsert
menus](https://firespark.cloud/docs/es/integrations-api/menus/put.md) para los query params.

**2. Arma el payload**

Envía entidades compartidas (`products`, `categories`, `modifier_groups`)
más hasta 10 entradas en `menus`. Cada menú referencia `category_ids` y un
`fulfillment_id`.

**3. Haz upsert del menú**

Llama `PUT /menus` para el baseline, o añade `?store_id=…` y/o
`?channel_id=…` para overrides. Usa `"style": "DEFAULT"` en canales propios,
o `"style": "UBER_EATS"` para formato Uber Eats Menu API v2. Ver [Upsert
menus](https://firespark.cloud/docs/es/integrations-api/menus/put.md).

**4. Verifica el resultado**

Lee menús compuestos con [List
menus](https://firespark.cloud/docs/es/integrations-api/menus/get.md?store_id=downtown\&channel_id=app). Usa
el mismo `store_id` y `channel_id` del upsert. Confirma `fulfillment_id`,
horarios e ids de producto contra tu mapeo POS.

**5. Envía cambios incrementales**

Para actualizaciones de un ítem — precio, 86, overrides por canal — usa
[Update product](https://firespark.cloud/docs/es/integrations-api/menus/products/\[id]/put) en lugar de
reenviar el menú completo.

## Varios canales, tiendas y modos de fulfillment

Cada menú compuesto se identifica por contexto:

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

Si omites `fulfillment_id`, `store_id`, `channel_id` o `brand_id`, esa dimensión es **todas**. Aprovéchalo: un upsert baseline con el catálogo completo y overrides solo donde haga falta.

| Escenario | Enfoque |
| - | - |
| Mismo catálogo en todas partes | Un solo `PUT /menus` sin query params; `schedules: null` para 24/7 |
| Un agregador necesita subset | Baseline (opcional), luego `PUT /menus?channel_id=uber-eats` con distintos `category_ids` — o solo ese canal |
| Una tienda tiene ítems regionales | Baseline (opcional), luego `PUT /menus?store_id=airport` — o solo esa tienda |
| Delivery vs pickup | `fulfillment_id` distinto en cada entrada de menú del payload — o ids de menú separados por fulfillment cuando el surtido difiere |
| Marca con varias etiquetas | Query param `brand_id` para acotar upserts por marca |

```mermaid theme={null}
flowchart TB
    POS["Un solo catálogo POS"]
    POS --> DEF["PUT /menus\n(default · cualquier tienda · cualquier canal)"]
    DEF --> A["?channel_id=uber-eats"]
    DEF --> B["?store_id=airport"]
    DEF --> C["fulfillment_id: pickup\n(en el payload)"]
```

### Overrides por ítem entre contextos

Cuando un producto necesita precio o disponibilidad distinta por canal, tienda o tipo de fulfillment sin reemplazar menús enteros, llama a [Update product](https://firespark.cloud/docs/es/integrations-api/menus/products/\[id]/put) con `?store_id=…`, `?channel_id=…`, `?fulfillment_id=…` y/o `?menu_id=…` cuando varios menús comparten el mismo scope. El body usa la misma forma de producto de menú que [Upsert menus](https://firespark.cloud/docs/es/integrations-api/menus/put.md) — `pricing` y `availability` en el nivel superior, no un objeto `channels` anidado.

Así piensa el equipo comercial: publica el producto base una vez y luego envía un update acotado con `?store_id` y `?channel_id` para que la Cola cueste \$0 en el combo de la app en downtown, mientras el catálogo default mantiene otro precio.

Las ediciones desde el dashboard o usando nuestro API de sincronización persisten en upserts normales. Para sobrescribir estos cambios, envía `x-force: true` en [Upsert menus](https://firespark.cloud/docs/es/integrations-api/menus/put.md). Para preparar una sincronización sin publicarla aún, envía `x-publish: false` y publícala desde el dashboard cuando esté lista.

## Horarios

Los horarios restringen cuándo un menú o entidad individual es pedible.

| Nivel | Campo | Formato |
| - | - | - |
| Menú | `menus[].schedules` | Array semanal (`monday`–`sunday`); cada periodo puede incluir `start_time` / `end_time`, `closed` opcional (por defecto `false`) y `start_date` / `end_date` opcionales |
| Producto / categoría | `availability.schedules` | Misma forma; aplica dentro del menú compuesto |
| Override de producto | `availability.schedules` en [Update product](https://firespark.cloud/docs/es/integrations-api/menus/products/\[id]/put) con `?store_id` / `?channel_id` / `?fulfillment_id` / `?menu_id` | Horas por contexto de venta o por menú compuesto |

Usa `null` o omite `schedules` cuando el menú está disponible todo el día — Fire spark lo interpreta como **24 horas, todos los días**. Define un array `schedules` solo cuando la pedibilidad se limita a días o franjas concretas.

Cuando envías un array `schedules`, Fire spark valida la forma semanal completa:

| Regla | Requisito |
| - | - |
| Cobertura semanal | Exactamente **7** entradas — una por día de `monday` a `sunday` |
| Días únicos | Cada `day_of_week` aparece **una sola vez** |
| Periodos por día | Como máximo **10** periodos |
| Campos del periodo | Los periodos abiertos requieren `start_time` y `end_time`. `closed` es opcional (por defecto `false`). `start_date` / `end_date` son nullable (ambos definidos o ambos `null`) |
| Sin solapamiento | Periodos con fechas null el mismo día no pueden solapar tiempos. Closed y open con fechas específicas no pueden compartir rango. Null + específico pueden coexistir |

Los tiempos en `start_time` y `end_time` usan strings ISO (por ejemplo `11:00:00`). Usa `closed: true` cuando el periodo es una ventana cerrada (`start_time` / `end_time` pueden omitirse). Usa fechas `null` como fallback siempre activo; define `start_date` / `end_date` para una ventana más específica. Un array `periods` vacío significa cerrado todo el día.

Varios menús en el mismo sync pueden tener franjas horarias solapadas. [Get menu](https://firespark.cloud/docs/es/storefront-api/menus/get.md) devuelve en `data` todos los menús compuestos cuyo horario coincide con la hora actual. Cuando ninguno está en horario, devuelve el o los menús cuya próxima ventana comienza antes. Los menús con `schedules: null` se consideran siempre activos.

**Patrones comunes:**

* **Default siempre activo** — Omite horarios (`null`) en el menú baseline; añade overrides con horario solo para dayparts que difieren.
* **Menús por daypart** — Ids de menú separados para desayuno y almuerzo en la misma tienda + canal + fulfillment, cada uno con su horario y `category_ids`.
* **Ventanas cerradas** — Mantén horas abiertas en un día y añade un periodo con `closed: true` (opcionalmente con `start_date` / `end_date`) para un feriado o bloqueo de mantenimiento.
* **Categorías estacionales** — Mantén el menú siempre activo; programa `availability` de la categoría o producto.
* **86 temporal** — Prefiere `availability.status: OUT_OF_STOCK` con `out_of_stock_until` para pausas de ítem sin reconstruir horarios.

## Reglas de validación

Fire spark valida el payload completo de [Upsert menus](https://firespark.cloud/docs/es/integrations-api/menus/put.md) antes de encolar un sync. Estas reglas aplican al estilo `DEFAULT`.

### Productos

| Campo | Regla |
| - | - |
| `products` | Al menos 1 entrada. Como máximo 1000. |
| `id`, `name`, `pricing`, `availability` | Requeridos en cada producto |
| `status` | **No aceptado** en productos de menu sync. Usa `availability.status`. |
| `availability.status` | `AVAILABLE`, `HIDDEN` u `OUT_OF_STOCK` |
| `availability.schedules` | Mismas reglas semanales que los horarios del menú cuando se envían. Omite o usa `null` sin restricción. |
| `availability.out_of_stock_until` | Fecha y hora ISO 8601 opcional |
| `labels` | Opcional. Como máximo 64 cadenas (1–128 caracteres cada una). Por defecto `[]`. |
| `tags` | Opcional. Objeto con valores boolean, string o number. Por defecto `{}`. |

### Categorías

| Campo | Regla |
| - | - |
| `categories` | Al menos 1 entrada. Como máximo 100. |
| `id`, `name`, `items`, `availability` | Requeridos en cada categoría |
| `status` | **No aceptado** en categorías de menu sync. Usa `availability.status`. |
| `items` | Al menos **1** entrada. Como máximo 1000. Cada entrada requiere `type` (`CATEGORY` o `PRODUCT`) e `id`. El orden del array es el orden de display. Sin ids duplicados dentro de la misma categoría. |
| `availability.status` | `ACTIVE`, `INACTIVE` o `HIDDEN` |
| `availability.schedules` | Mismas reglas semanales que los horarios del menú cuando se envían. Omite o usa `null` sin restricción. |
| `availability.inactive_until` | Fecha y hora ISO 8601 opcional |
| `labels` | Opcional. Como máximo 64 cadenas (1–128 caracteres cada una). Por defecto `[]`. |
| `tags` | Opcional. Objeto con valores boolean, string o number. Por defecto `{}`. |

**Anidamiento** (ver [Subcategorías anidadas](#subcategorías-anidadas)):

| Regla | Requisito |
| - | - |
| Referencias | Cada `items[].id` debe existir en `products[]` (si `type` es `PRODUCT`) o en `categories[]` (si `type` es `CATEGORY`) |
| Alcance | Toda categoría debe estar en `menus[].category_ids` o referenciada como hija en `items` de otra categoría |
| Profundidad | Máximo **3** niveles desde cada raíz en `category_ids`, contando la raíz como nivel 1 |
| Ciclos | Una categoría no puede ser ancestro de sí misma |

### Definiciones de menú

| Campo | Regla |
| - | - |
| `menus` | Al menos 1 entrada. Como máximo 10. |
| `id`, `name`, `category_ids`, `status` | Requeridos en cada menú |
| `category_ids` | Al menos **1** id de categoría. Como máximo 100. Solo categorías **raíz** del árbol. El orden del array es el orden de display. Sin ids duplicados. |
| `status` | `ACTIVE` o `INACTIVE` |
| `schedules` | Opcional. `null` u omitido para 24/7. Si se envía, debe cumplir las reglas semanales anteriores. |

### Grupos de modificadores

| Campo | Regla |
| - | - |
| `modifier_groups` | Opcional. Como máximo 1000 cuando se envían. |
| `modifier_ids` | Al menos **2** entradas por grupo |

### Recomendaciones

| Campo | Regla |
| - | - |
| `recommendations` | Opcional. Como máximo 1000 cuando se envían. Default `[]`. |
| `id`, `title`, `type`, `target_product_id` | Requeridos en cada recomendación |
| `type` | `UPSELL`, `CROSS_SELL` o `DOWNSELL` |
| `target_product_id` | Debe referenciar un `id` de `products` |
| `pricing.recommendations_ids` | Opcional por producto. Cada id debe existir en `recommendations` |
| `pricing.recommendations` | Opcional por producto. Cada `recommendation_id` debe existir en `recommendations`. Sin ids duplicados por producto |
| `pricing.recommendations[].default_quantity` | Opcional. Default `0`. Independiente de `pricing.modifiers[].default_quantity` |

Cuando un sync define varios menús, Fire spark compone cada menú desde el catálogo compartido y conserva solo las recomendaciones cuyo `target_product_id` es alcanzable en ese menú — las mismas reglas de composición que `products` y `modifier_groups`.

### Labels y tags

Los productos y categorías aceptan `labels` y `tags` opcionales. Omite cualquiera de los dos en syncs existentes — Fire spark usa `[]` y `{}` por defecto y no rechaza payloads que los dejen fuera.

| Campo | Forma | Uso |
| - | - | - |
| `labels` | `string[]` | Filtros y selección rápida en el Dashboard. |
| `tags` | `{ [key: string]: boolean \| string \| number }` | Casos avanzados como reglas personalizadas. Ver [Reglas personalizadas](https://firespark.cloud/docs/es/integrations-api/guides/custom-rules). |

```json theme={null}
{
  "id": "hamburguesa-clasica",
  "labels": ["destacado", "almuerzo"],
  "tags": { "promo_eligible": true, "kitchen_station": "grill" }
}
```

> [!WARNING]
>
> La disponibilidad de categorías usa `ACTIVE`, `INACTIVE` y `HIDDEN`. La de
> productos usa `AVAILABLE`, `HIDDEN` y `OUT_OF_STOCK`. Ambos comparten
> `HIDDEN`. Las categorías usan `inactive_until`, no `out_of_stock_until`.

## Estilos de sync

[Upsert menus](https://firespark.cloud/docs/es/integrations-api/menus/put.md) acepta una unión discriminada por `style`:

> [tab] DEFAULT
>
> Formato nativo Fire spark para canales propios (app, web, kiosco, call
> center). Usa nombres de campo `products`, `categories`, `modifier_groups` y
> objetos `name` localizados por locale (por ejemplo `en_us`).
>
> Ideal cuando controlas la UI del cliente con la
> [Storefront API](https://firespark.cloud/docs/es/storefront-api/introduction.md) o un front end propio.

> [tab] UBER\_EATS
>
> Payload Uber Eats Menu API v2. Productos y modificadores comparten el array
> `items`; los precios son centavos enteros en `price_info.price`.
>
> Ideal para empujar directo a un canal agregador Uber Eats sin mapear campos
> tú mismo. Ver los
> [example menu payloads](https://developer.uber.com/docs/eats/references/api/v2/example-menu-payloads)
> de Uber para modificadores anidados e ítems incluidos.

## Sync completa vs actualizaciones incrementales

| Método | Endpoint | Cuándo usarlo |
| - | - | - |
| Upsert de menú completo | [Upsert menus](https://firespark.cloud/docs/es/integrations-api/menus/put.md) | Carga inicial, cambios estructurales (categorías nuevas, grupos) o recomponer `category_ids` |
| Producto individual | [Update product](https://firespark.cloud/docs/es/integrations-api/menus/products/\[id]/put) | Cambio de precio, 86 u override por canal en un ítem |
| Lectura | [List menus](https://firespark.cloud/docs/es/integrations-api/menus/get.md) | Reconciliación y verificación de mapeo por tienda y canal (`?store_id` + `?channel_id`) |

> [!WARNING]
>
> Los `id` de menú son inmutables tras la creación. Envía el mismo `id` externo
> de menú en cada sync para un contexto de venta dado.

## Mapea ids de vuelta a tu POS

| Campo Fire spark | Mapear a |
| - | - |
| `products[].id` | Ítem POS / PLU / SKU |
| `categories[].id` | Sección o código de categoría en el POS |
| `menus[].id` | Versión de menú o identificador de daypart en el RMS |
| `fulfillment_id`, `store_id`, `channel_id`, `brand_id` | Ubicación, superficie de venta y modo de servicio en tu RMS |

Usa ids externos para mapeo entre sistemas. Usa campos `*_uid` solo al referenciar recursos Fire spark en otras llamadas API.

## Endpoints relacionados

> [card] [Upsert menus](https://firespark.cloud/docs/es/integrations-api/menus/put.md)
>
> Crear o actualizar menús compuestos

> [card] [List menus](https://firespark.cloud/docs/es/integrations-api/menus/get.md)
>
> Leer menús para reconciliación

> [card] [Update product](https://firespark.cloud/docs/es/integrations-api/menus/products/\[id]/put)
>
> Enviar cambios por ítem

> [card] [Order webhooks](https://firespark.cloud/docs/es/integrations-api/orders/webhooks.md)
>
> Recibir pedidos con los mismos ids de producto

> [card] [List stores](https://firespark.cloud/docs/es/integrations-api/stores/get.md)
>
> Leer ids externos de tienda antes que menús

> [card] [List fulfillment](https://firespark.cloud/docs/es/integrations-api/fulfillment/get.md)
>
> Leer ids externos de fulfillment antes que menús

## Payloads de ejemplo

Payloads listos para copiar en escenarios comunes de sync de menú:

> [card] [Menú simple](https://firespark.cloud/docs/es/integrations-api/examples/menu-sync/simple-menu.md)
>
> 3 productos, 1 categoría, 1 grupo de modificadores

> [card] [Subcategorías anidadas](https://firespark.cloud/docs/es/integrations-api/examples/menu-sync/nested-categories.md)
>
> Categoría raíz con dos subcategorías

> [card] [Multi horario](https://firespark.cloud/docs/es/integrations-api/examples/menu-sync/multi-schedule.md)
>
> 2 menús daypart, sin grupos de modificadores

> [card] [Modificadores multinivel](https://firespark.cloud/docs/es/integrations-api/examples/menu-sync/multi-level-modifiers.md)
>
> 3 niveles de grupos anidados

> [card] [Overrides de grupos](https://firespark.cloud/docs/es/integrations-api/examples/menu-sync/modifier-group-overrides.md)
>
> Overrides de precio, min y max

> [card] [Horario de producto](https://firespark.cloud/docs/es/integrations-api/examples/menu-sync/product-special-schedule.md)
>
> Disponibilidad happy hour en un producto

> [card] [Recomendaciones de producto](https://firespark.cloud/docs/es/integrations-api/examples/menu-sync/product-recommendations.md)
>
> Upsell a combo, cross-sell de acompañamiento y downsell a producto suelto


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.