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.
The menu model
Section titled “The menu model”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 modifiersEvery 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.
| Object | Required fields | Notable optional fields |
|---|---|---|
| menu | name | external_id, description |
| category | name | external_id, description, image_url, tax_exempt, tax |
| item | external_id, name, price | description, image_url, available, categories[], addon_groups[], product_type, tax_exempt, tax, variant_type, variants[] |
| variant | external_id, price | variant_name, variant_key, stock, image_url, available, consumption_ratio, combinations[] |
| addon_group | external_id, name, add_ons[] | min_select, max_select, required |
| add_on | external_id, name, price | as 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_type —
stand_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.
Full menu push
Section titled “Full menu push”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.
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.jsonconst response = await fetch( 'https://auth.quiqqy.ai/api/v2/pos/stores/st_8fk2c1p9/menus', { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify(menu), // see the sample below },);
if (response.status === 202) { const { poll_url } = await response.json(); await pollMenuJob(poll_url);}import requests
response = requests.post( "https://auth.quiqqy.ai/api/v2/pos/stores/st_8fk2c1p9/menus", headers={"Authorization": f"Bearer {access_token}"}, json=menu, # see the sample below)
if response.status_code == 202: poll_menu_job(response.json()["poll_url"])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[].
200 sync, 202 async
Section titled “200 sync, 202 async”Small pushes apply synchronously:
{ "status": "success", "message": "Menu pushed to Quiqqy" }Pushes above ~100 items are processed as a background job:
{ "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:
{ "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.
Batch updates
Section titled “Batch updates”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.
When Quiqqy asks: menu_push
Section titled “When Quiqqy asks: menu_push”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.
Checking sync state
Section titled “Checking sync state”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"}