Skip to content

Menu sync

Quiqqy never guesses your catalogue: you push it, and what you push is what customers can order. The POS is the source of truth; Quiqqy mirrors it.

Menu sync requires an active integration with menu_sync_direction of pos_to_quiqqy or bidirectional — anything else rejects pushes with menu.push_not_allowed.

A push is a hierarchy keyed entirely on your identifiers:

menus[] named menus (e.g. "Main", "Breakfast")
└── categories[] sections within a menu
└── items[] sellable products
├── variants[] SKUs of an item (Small / Large)
└── addon_groups[] modifier groups attached to the item
└── add_ons[] the selectable modifiers

Every object carries an external_id — your POS’s stable id for it. This is the idempotency key for sync (pushing the same external_id updates rather than duplicates) and the id that comes back to you on order webhooks as items[].external_id, items[].variant_external_id and options[].external_id. Choose ids that never change for the life of the record; never reuse an id for a different product.

ObjectRequired fieldsNotable optional fields
menunameexternal_id, description
categorynameexternal_id, description, image_url, tax_exempt, tax
itemexternal_id, name, pricedescription, image_url, available, categories[], addon_groups[], product_type, tax_exempt, tax, variant_type, variants[]
variantexternal_id, pricevariant_name, variant_key, stock, image_url, available, consumption_ratio, combinations[]
addon_groupexternal_id, name, add_ons[]min_select, max_select, required
add_onexternal_id, name, priceas item; product_type: "addon" implied

Prices are integers in the currency’s minor unit (1299 = $12.99).

Items versus add-ons. items[] holds only directly-sellable products. A modifier that can only be ordered as part of an item (a topping, a sauce) belongs in add_ons[], wired to items through addon_groups[] — it never appears standalone in the storefront. If your POS exports a flat catalogue where modifiers are typed products, set product_type: "addon" on the item instead of pre-splitting your export; the effect is the same.

Variants. An item sold in variations carries variant_typestand_alone (each variant has its own price and stock) or consumption_ratio (variants draw from the parent’s stock at a ratio) — plus variants[]. A push replaces the item’s variant set exactly: a variant you stop sending is deactivated.

POST /pos/stores/:storeId/menus replaces the store’s synced menu with what you send. Push the whole catalogue — on first sync, whenever the POS menu changes, and whenever Quiqqy asks for it.

Terminal window
curl -s https://auth.quiqqy.ai/api/v2/pos/stores/st_8fk2c1p9/menus \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d @menu.json

A compact body (the full realistic version is in Sample payloads):

{
"menus": [
{
"name": "Main Menu",
"external_id": "menu-main",
"categories": [{ "external_id": "cat-pizza", "name": "Pizza" }],
"items": [
{
"external_id": "SKU-MARG",
"name": "Margherita Pizza",
"price": 1299,
"categories": ["cat-pizza"],
"addon_groups": ["grp-toppings"],
"variant_type": "stand_alone",
"variants": [
{ "external_id": "SKU-MARG-SM", "variant_name": "Small", "price": 1299 },
{ "external_id": "SKU-MARG-LG", "variant_name": "Large", "price": 1699 }
]
}
]
}
],
"add_ons": [{ "external_id": "ADD-BASIL", "name": "Extra basil", "price": 150 }],
"addon_groups": [
{
"external_id": "grp-toppings",
"name": "Toppings",
"min_select": 0,
"max_select": 3,
"add_ons": ["ADD-BASIL"]
}
]
}

A flat form also exists — top-level menu_name + items[]/categories[] without the menus[] wrapper — for single-menu stores. Prefer menus[].

Small pushes apply synchronously:

200 OK
{ "status": "success", "message": "Menu pushed to Quiqqy" }

Pushes above ~100 items are processed as a background job:

202 Accepted
{
"status": "accepted",
"job_id": "job_5tq8w2ma",
"poll_url": "/api/v2/pos/stores/st_8fk2c1p9/menus/jobs/job_5tq8w2ma"
}

Poll GET /pos/stores/:storeId/menus/jobs/:jobId until it settles:

200 OK
{
"job_id": "job_5tq8w2ma",
"status": "completed",
"items_processed": 412,
"errors": []
}

status is queued, processing, completed or failed. Poll with backoff (a few seconds is plenty); handle both branches from one code path by treating 200 as an already-completed job. errors[] names offending external_ids — fix and re-push; a partial failure never half-applies an item.

For deltas between full pushes, POST /pos/stores/:storeId/menus/batch applies adds, updates and deletes in one call:

{
"add": [{ "external_id": "SKU-TIRAMISU", "name": "Tiramisu", "price": 750 }],
"update": [{ "external_id": "SKU-MARG", "price": 1399 }],
"delete": ["SKU-CALZONE"]
}

A single item can also be updated in place with PATCH /pos/stores/:storeId/items/:extItemId — send only the fields that change. For stock flips specifically, use the dedicated availability endpoints.

Quiqqy sometimes needs your menu — after an integration is repaired, when a merchant or Quiqqy support triggers a re-sync from the console. It asks over your webhook with an action_required event:

{
"event_type": "action_required",
"data": { "action": "menu_push", "integration_id": "int_3a2c8d1e" }
}

Acknowledge with 2xx, then perform a full menu push. data.action may also be resync — treat it identically (data.request_type is a legacy alias of data.action). Payload details are in Webhook events.

GET /pos/stores/:storeId/menus/status returns the direction, integration status and last successful sync:

{
"store_id": "st_8fk2c1p9",
"menu_sync_direction": "pos_to_quiqqy",
"integration_status": "active",
"last_synced_at": "2026-07-30T09:12:44.000Z"
}