Skip to content

Catalogue

The catalogue is what the point of sale shows and what every sale line refers back to. An item belongs to a category, a brand and a tax; a menu decides which items a channel sees; modifiers add the questions asked at the till.

MethodPathScope
GET/v1/itemsitems.read
GET/v1/items/:iditems.read
GET/v1/items/:id/fullitems.read
POST/v1/itemsitems.write
PATCH/v1/items/:iditems.write
GET/v1/categoriescategories.read
POST/v1/categoriescategories.write
PATCH/v1/categories/:idcategories.write
GET/v1/brandsbrands.read
POST/v1/brandsbrands.write
PATCH/v1/brands/:idbrands.write
GET/v1/unitsitems.read
POST/v1/unitsitems.write
GET/v1/kitchensitems.read
GET/v1/variant-attributesitems.read
GET/v1/modifiersmodifiers.read
POST/v1/modifiersmodifiers.write
PATCH/v1/modifiers/:idmodifiers.write
GET/v1/menusmenus.read
GET/v1/menus/:id/fullmenus.read
POST/v1/menus/create-with-itemsmenus.write
PATCH/v1/menus/:id/update-with-itemsmenus.write
GET/v1/combo-slotsmeal_slots.read
POST/v1/combo-slotsmeal_slots.write
PUT/v1/items/:id/mealmeal_slots.write

An item cannot exist before the rows it points at. For a new catalogue, create in this order:

  1. Taxes — POST /v1/taxes. See taxes.
  2. Categories — POST /v1/categories.
  3. Brands — POST /v1/brands.
  4. Units — GET /v1/units (create one with POST /v1/units if the merchant has none you can use).
  5. Items — POST /v1/items, referencing the ids above.
  6. Modifiers — POST /v1/modifiers, attaching them to items.
  7. Menus — POST /v1/menus/create-with-items, selecting items per channel.

Kitchens are optional and never need creating: GET /v1/kitchens lists the ones the merchant already runs, and an item’s kitchenId routes its ticket to one of them.

Syncing an existing catalogue? Read the merchant’s current categories, brands and taxes first and map onto them. Creating duplicates of rows the merchant already curates is the most common review failure.

Terminal window
curl -s "$API_BASE_URL/categories" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "groupName": "Hot drinks", "priority": 10, "kitchenId": 2 }'
FieldTypeRequiredNotes
groupNamestringyesUp to 50 characters, unique in practice
prioritynumbernoDisplay order; lower sorts first. Default 0
kitchenIdnumbernoRoutes the category’s tickets to a kitchen printer or KDS
statusnumberno1 active (default), 0 hidden
favouritenumberno1 pins the category on the till’s quick grid
IsEBTnumbernoUS only — marks the category as EBT-eligible
Terminal window
curl -s "$API_BASE_URL/brands" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "brand": "House", "priority": 1 }'

brand (≤ 50 characters) is the only required field; priority and status behave as they do on categories. Every item needs a brand — merchants that do not think in brands keep a single House row.

Three small reference lists that item writes lean on. All three are item reference data, so they use the item scopes: items.read to list, items.write to create a unit.

Every item needs a uomId. Read the merchant’s list first and map onto it.

Terminal window
curl -s "$API_BASE_URL/units?limit=100" \
-H "Authorization: Bearer $ACCESS_TOKEN"
{
"data": [
{ "id": 1, "name": "Piece", "uom": "PCS", "decPlace": 0, "status": 1 }
],
"meta": { "page": 1, "limit": 100, "total": 1 }
}

Create one only when nothing in that list fits:

Terminal window
curl -s "$API_BASE_URL/units" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "name": "Kilogram", "uom": "KG", "decPlace": 3 }'
FieldTypeRequiredNotes
namestringyesDisplay name, up to 30 characters
uomstringyesShort code printed on documents, up to 30 characters
decPlacenumberyesDecimal places allowed on a quantity — 0 for whole units, 3 for weights
multiplenumbernoConversion multiple for the unit. Default 1
statusnumberno1 active (default), 0 inactive
Terminal window
curl -s "$API_BASE_URL/kitchens" \
-H "Authorization: Bearer $ACCESS_TOKEN"
{
"data": [
{ "id": 2, "name": "Grill", "status": 1 }
],
"meta": { "page": 1, "limit": 10, "total": 1 }
}

A kitchen is a printer or KDS destination. Set kitchenId on a category to route everything in it, or on an item to override the category.

Terminal window
curl -s "$API_BASE_URL/variant-attributes" \
-H "Authorization: Bearer $ACCESS_TOKEN"
{
"data": [
{ "id": 2, "name": "Size" },
{ "id": 5, "name": "Colour" }
]
}

These are the attribute names — Size, Colour, Flavour — the merchant already uses on variant items. Read the list before you build a variants.attributes block so that your “size” lands on the merchant’s existing Size rather than creating a near-duplicate. Names are matched case-insensitively on write, and a name that is genuinely new is created for the merchant.

An item is one line at the till. Every item has a type, the type is fixed when the item is created, and the type decides what else the body may carry.

typeItemWhat it carries
0Standard (default)One sellable line with one price per store
1VariantA matrix of SKUs — Size × Colour — each with its own code, barcode and price
2CompositeA fixed bundle sold and priced as one line, assembled from other items

The type is fixed at creation. A PATCH that names a different type is refused with 409 partner.item_type_immutable, because changing it would orphan the SKUs or components already attached. Send the same value, or leave type out.

Terminal window
curl -s "$API_BASE_URL/items" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"item": "Flat White",
"groupId": 12,
"brandId": 1,
"uomId": 1,
"taxId": 3,
"rate": 3.75,
"mrp": 4.00,
"barcode": "8901234567890",
"ItemCode": "COF-FW-REG",
"Description": "Double ristretto, steamed milk",
"stockable": 0,
"CanOnlineSale": 1,
"IsVeg": true,
"kitchenId": 2
}'

That body carries no stores, so rate is applied at every active store the merchant has. That fan-out is what makes the item sellable at the till: an item with no store price is not on sale anywhere. Warehouses are skipped, and a store-scoped grant narrows the fan-out to the stores your app was granted.

Send stores when the price differs by store, or when you want to open the item with stock:

Terminal window
curl -s "$API_BASE_URL/items" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"item": "Flat White",
"groupId": 12,
"brandId": 1,
"uomId": 1,
"taxId": 3,
"rate": 3.75,
"stores": [
{ "storeId": 1, "price": 3.75, "openingStock": 20, "cost": 1.1 },
{ "storeId": 2, "price": 3.95 }
]
}'

On create, a store you leave out of stores gets no row at all — list every store the item should sell in, or leave stores out entirely (an empty list means the same thing) and let rate fan out.

FieldTypeRequiredNotes
itemstringyesDisplay name, up to 100 characters
groupIdnumberyesCategory id
brandIdnumberyesBrand id
uomIdnumberyesUnit of measure id — GET /v1/units
taxIdnumberyesSales tax row applied at the till
typenumberno0 standard (default), 1 variant, 2 composite. Fixed at creation
item1stringnoSecondary name — used for a second language on receipts
ratenumbernoSelling price. On a standard or composite item it is applied at every active store unless stores says otherwise; on a variant item pricing lives on the SKUs
mrpnumbernoMaximum retail price, where regulation requires it printed
barcodestringnoSingle barcode. One value, not a list
ItemCodestringnoYour own SKU. The field to correlate on when syncing
hsnstringnoTax classification code (HSN/SAC), up to 35 characters
DescriptionstringnoLong description for online channels
statusnumberno1 active (default), 0 inactive
stockablenumberno1 tracks stock (default), 0 does not
canSalenumberno1 sellable at the till (default), 0 hides it while keeping it for recipes
CanOnlineSalenumberno1 exposes the item to online ordering (default)
kitchenIdnumbernoOverrides the category’s kitchen routing
hasBatchnumberno1 enables batch tracking
ExpiryTracknumberno1 requires expiry dates on receipt
weighingScalenumberno1 for scale-priced goods. Default 0, and always 0 on a variant item
serialNonumberno1 requires a serial number per unit. Default 0
favouritenumberno1 pins the item on the till’s quick grid. Default 0
eachItembooleannoDefault false
IsVegbooleannoVegetarian marker used on menus. Default false
IsServicebooleannoService line rather than a physical good. Default false
IsEBTnumbernoUS only — marks the item as EBT-eligible. Default 0
IsAlcaholbooleannoAlcohol marker (spelling is part of the wire contract). Default false
HealthyInfostringnoNutrition or allergen note shown on online channels
PurchTaxIdnumbernoPurchase-side tax, when it differs from the sales tax
cessId, Tax4IdnumbernoAdditional levies in jurisdictions that use them
PurchCessId, PurchTax4IdnumbernoPurchase-side equivalents
modifierIdsnumber[]noModifier groups attached to the item, up to 50 ids
prepTimenumber | nullnoPreparation time in minutes. null clears it
caloriesnumber | nullnoEnergy in kcal. null clears it
dietaryTagsstring[]noUp to 50 tags of up to 40 characters. [] clears them
storesobject[]noPer-store price and stock. Standard and composite items only — see below
variantsobjecttype: 1Attributes and SKUs — see A variant item
comboobject[]type: 2Components of the bundle — see A composite item

Never send userId, Modifiers, modifierId, imageId or Images: the actor is derived from your token, modifier groups are attached with modifierIds, and item images are back office only.

Up to 200 rows, on standard and composite items. A variant item carries no item-level stores — its SKUs do. On create, omitting the list and sending an empty one mean the same thing: sell at every active store at rate. A storeId may appear only once per list.

FieldTypeRequiredNotes
storeIdnumberyesMust be an active store of the merchant, and one your app was granted
pricenumbernoSale price at this store. Defaults to the item’s rate
minPricenumbernoLowest price the till will accept. Default 0
markupnumbernoMarkup % over cost (the back office’s Markup % column). Default 0 on create; an update that omits it keeps the stored value
costnumbernoUnit cost used to value openingStock. Default 0
openingStocknumbernoCreate only, stockable items only. Ignored on PATCH
reorderPointnumbernoLow-stock threshold. Default 0
statusnumberno1 sold at this store (default), 0 not
201 Created (abridged)
{
"data": {
"id": 8841,
"type": 0,
"item": "Flat White",
"groupId": 12,
"brandId": 1,
"uomId": 1,
"taxId": 3,
"rate": 3.75,
"status": 1,
"modifierIds": [],
"stores": [
{ "id": 77, "storeId": 1, "storeName": "Brigade Road", "price": 3.75, "minPrice": 0, "markup": 0, "cost": 1.1, "reorderPoint": 0, "status": 1 }
],
"variants": null,
"combo": []
}
}

POST and PATCH both return the item definition — the same shape as GET /v1/items/:id/full, abridged here.

One product, many SKUs. Declare the attributes, then every SKU the merchant sells.

Terminal window
curl -s "$API_BASE_URL/items" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"item": "House T-shirt",
"type": 1,
"groupId": 12,
"brandId": 1,
"uomId": 1,
"taxId": 3,
"ItemCode": "TS-001",
"variants": {
"attributes": [
{ "name": "Size", "values": ["S", "M", "L"] },
{ "name": "Colour", "values": ["Black", "White"] }
],
"skus": [
{ "options": { "Size": "S", "Colour": "Black" }, "itemCode": "TS-001-S-BK", "barcode": "8901234500011", "price": 9.5 },
{ "options": { "Size": "M", "Colour": "Black" }, "itemCode": "TS-001-M-BK", "barcode": "8901234500012", "price": 9.5 },
{
"options": { "Size": "M", "Colour": "White" },
"itemCode": "TS-001-M-WH",
"barcode": "8901234500013",
"price": 10.5,
"stores": [
{ "storeId": 1, "price": 10.5, "openingStock": 12, "cost": 4.2 },
{ "storeId": 2, "price": 11.0 }
]
}
]
}
}'

You do not have to send the whole matrix. Three of the six combinations are listed above and three simply do not exist for this merchant.

One to five attributes.

FieldTypeRequiredNotes
namestringyes1–20 characters. Matched case-insensitively against the merchant’s existing attributes; a new name creates one
valuesstring[]yes1–50 values, each 1–200 characters

One to 500 SKUs.

FieldTypeRequiredNotes
idnumbernoAn existing SKU id. PATCH only — leave it out when creating
optionsobjectyesAttribute name to value, one entry per declared attribute
itemCodestringnoSKU code, up to 45 characters
barcodestringnoUp to 45 characters
pricenumbernoDefault sale price for this SKU
minPricenumbernoLowest price the till will accept
mrpnumbernoMaximum retail price
markupnumbernoMarkup % over cost for this SKU. Default 0 on create; an update that omits it keeps the stored value. SKU store rows carry no markup
statusnumberno1 active (default), 0 inactive
storesobject[]noPer-store rows for this SKU, up to 200. Omitted means price fans out to every active store
FieldTypeRequiredNotes
storeIdnumberyesMust be an active store of the merchant, and one your app was granted
pricenumbernoDefaults to the SKU’s price
minPricenumberno
costnumbernoUnit cost used to value openingStock
openingStocknumbernoCreate only. Ignored on PATCH
reorderPointnumbernoLow-stock threshold
reorderQtynumbernoQuantity suggested when reordering
statusnumberno1 sold at this store (default), 0 not

The rules the validator enforces:

  • Every SKU names every declared attribute, with a value that attribute declares. A SKU that skips one is rejected with partner.variant_sku_incomplete; an unknown name or value with partner.variant_option_unknown.
  • Attribute names and values are matched case-insensitively, so "size": "m" finds the merchant’s Size / M. A name nobody has used before is created for the merchant — list what exists with GET /v1/variant-attributes first.
  • Attribute names must be unique within variants.attributes — they are compared case-insensitively after trimming, so Size and size are the same attribute (partner.variant_attribute_duplicate).
  • No two SKUs may share the same option combination, and no two SKUs in one update may resolve to the same stored SKU (partner.variant_sku_duplicate).
  • A variant item carries no item-level stores: store pricing lives on the SKUs. Prices come from the SKUs too, so leave rate to the read side — it comes back as the average of the SKU store prices, and an attempt to PATCH it is refused with catalogue_products.variant_item_price_update_blocked.

A composite item is a bundle sold as one line: one till button, one price, several items behind it.

Terminal window
curl -s "$API_BASE_URL/items" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"item": "Lunch deal",
"type": 2,
"groupId": 12,
"brandId": 1,
"uomId": 1,
"taxId": 3,
"rate": 12,
"combo": [
{ "itemId": 8850, "qty": 1 },
{ "itemId": 8851, "qty": 1, "choiceGroup": "drink" },
{ "itemId": 8852, "qty": 1, "choiceGroup": "drink", "price": 0.5 },
{ "itemId": 8853, "optional": true }
]
}'

rate is the price of the whole bundle and fans out to every active store, exactly as it does on a standard item. stores works here too when the deal costs more in one location.

One to 100 components; at least one is required when creating.

FieldTypeRequiredNotes
idnumbernoAn existing component row id. PATCH only
itemIdnumberyesThe component item. It must exist and must not itself be a composite
variantIdnumbernoThe SKU, when the component is a variant item. 0 or omitted otherwise
qtynumbernoUnits of the component in the bundle. Default 1
pricenumbernoWhat this component costs inside the combo. Default 0, meaning included in the bundle price
optionalbooleannotrue offers the component instead of always including it. Default false
choiceGroupstringnoUp to 45 characters. Components sharing a group are a pick-one choice

In the example: item 8850 is always in the deal. Items 8851 and 8852 share the group drink, so the cashier picks one of the two — and picking 8852 adds 0.50 to the line, because that is its price inside the combo. Item 8853 is optional: offered, never automatic.

GET /v1/items/:id returns the list row — the projection you get from GET /v1/items, with rate as the average active-store price. GET /v1/items/:id/full returns the definition: the same vocabulary you write.

Terminal window
curl -s "$API_BASE_URL/items/8841/full" \
-H "Authorization: Bearer $ACCESS_TOKEN"
200 OK
{
"data": {
"id": 8841, "type": 1,
"item": "T-shirt", "item1": null, "Description": null, "ItemCode": "TS-001", "barcode": "", "hsn": null,
"groupId": 12, "brandId": 1, "uomId": 1, "taxId": 3, "PurchTaxId": 3, "cessId": 0, "PurchCessId": 0, "Tax4Id": 0, "PurchTax4Id": 0,
"kitchenId": 0, "mrp": 0, "rate": 9.5,
"status": 1, "stockable": 1, "canSale": 1, "CanOnlineSale": 1, "eachItem": false, "favourite": 0,
"weighingScale": 0, "serialNo": 0, "hasBatch": 0, "ExpiryTrack": 0,
"IsVeg": false, "IsService": false, "IsEBT": 0, "IsAlcahol": false,
"HealthyInfo": null, "imageId": null,
"prepTime": null, "calories": null, "dietaryTags": [],
"modifierIds": [4],
"stores": [],
"variants": {
"attributes": [ { "id": 2, "name": "Size", "values": [ { "id": 31, "name": "S" }, { "id": 32, "name": "M" } ] } ],
"skus": [
{ "id": 501, "options": { "Size": "S" }, "itemCode": "TS-001-S", "barcode": "", "price": 9.5, "minPrice": 0, "mrp": 0, "markup": 0, "status": 1,
"stores": [ { "id": 77, "storeId": 1, "price": 9.5, "minPrice": 0, "cost": 0, "reorderPoint": 0, "reorderQty": 0, "status": 1 } ] }
]
},
"combo": []
}
}

The flat keys are the ones you wrote, spelled the same way. These are the keys the list row does not have:

KeyNotes
type0 standard, 1 variant, 2 composite
stores{ id, storeId, storeName, price, minPrice, markup, cost, reorderPoint, status } per store. Empty on a variant item. Live stock is not part of the definition — read it from inventory
variantsattributes with their value ids, and skus (each carrying markup) with their store rows. null on a non-variant item
combo{ id, itemId, itemName, variantId, qty, price, optional, choiceGroup, status } per component. [] on a non-composite item, and rows materialised from meal slots are excluded — those belong to PUT /v1/items/:id/meal
modifierIdsIds of the modifier groups attached to the item
prepTime, calories, dietaryTagsPreparation minutes, kcal and the tag list

variants.skus[].id is the variantId that menus, online orders, purchase orders and stock transfers carry. Read it here once, store it against your own SKU, and send it on every line for that SKU.

rate is the average active-store price, and 0 when no store row carries a price.

Terminal window
curl -s "$API_BASE_URL/items?search=flat&groupId=12&status=1&limit=100" \
-H "Authorization: Bearer $ACCESS_TOKEN"
ParameterNotes
searchMatches name, code and barcode
groupIdOne or more category ids, comma-separated
brandId, taxIdSingle id
status1 or 0
stockStatusinStock, low or out
page, limit, sort, orderStandard paging

PATCH accepts any subset of the create fields. Send only what changes:

Terminal window
curl -s -X PATCH "$API_BASE_URL/items/8841" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "rate": 3.95 }'
What you sendWhat happens
A scalar fieldWritten. A field you leave out is untouched
rateOn a standard or composite item: every active store your app is granted is re-priced — each existing row at such a store, and every such store with no row yet, gets price = rate. Rows at archived stores, at warehouses, or at stores outside a store-scoped grant are left as they are. A stores[] sent in the same body is a set of per-store overrides: a store you list with its own price keeps that price, so { "rate": 10, "stores": [] } re-prices everything. Divergent store prices are overwritten — that is what one rate means. On a variant item the request is refused with catalogue_products.variant_item_price_update_blocked; price the SKUs instead
storesEach listed row is upserted, matched by storeId; values you omit on a listed store keep their stored values (including markup). On its own, an empty stores list touches nothing; when rate is in the same body, stores[] is the set of overrides described in the rate row above. Stores you do not list are untouched. openingStock is ignored — move stock with an adjustment
variantsThe complete definition. attributes is the whole attribute list, so a value you leave out is deactivated. skus is the whole SKU list: each is matched by id, or failing that by its option combination; a matched SKU keeps every scalar you omit, and one whose stores you omit keeps its store rows. SKUs you leave out are retired (soft-deleted; sales history is kept). New SKUs without stores get the usual fan-out
comboThe complete component list. Components are matched by id, or by itemId and variantId. Components you leave out are disabled; new ones are added
modifierIdsThe complete modifier list for the item
prepTime, caloriesnull clears the value; omitting the key leaves it alone
dietaryTags[] clears the tags; omitting the key leaves them alone
typeIgnored when it equals the stored type; 409 partner.item_type_immutable when it differs

A nested collection you send replaces the stored one — except stores, where only the rows you list are written; stores you do not list are untouched. A nested collection you omit is not touched at all, so a price change never needs the SKU list attached.

Terminal window
# Per-store price, one store only — store 1 is untouched
curl -s -X PATCH "$API_BASE_URL/items/8841" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "stores": [{ "storeId": 2, "price": 4.25 }] }'
# The SKU list is complete: the SKU left out of this body is retired
curl -s -X PATCH "$API_BASE_URL/items/8900" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"variants": {
"attributes": [{ "name": "Size", "values": ["S", "M"] }],
"skus": [
{ "id": 501, "options": { "Size": "S" }, "price": 10.5 },
{ "id": 502, "options": { "Size": "M" }, "price": 10.5 }
]
}
}'
# Swap a component out of the bundle
curl -s -X PATCH "$API_BASE_URL/items/8910" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "combo": [{ "itemId": 8850, "qty": 1 }, { "itemId": 8854, "qty": 1, "choiceGroup": "drink" }] }'
# Re-attach modifier groups, and clear the dietary tags
curl -s -X PATCH "$API_BASE_URL/items/8841" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "modifierIds": [4, 6], "dietaryTags": [] }'

Deactivate rather than delete — {"status": 0} keeps the item’s sales history intact while removing it from the till.

Item writes branch on code, never on the message text — messages arrive in the caller’s language.

CodeHTTPMeans
partner.item_type_immutable409The PATCH names a type other than the stored one
partner.item_variants_required400type: 1 created without variants, or with an empty attributes or skus list
partner.item_variants_not_allowed400variants sent for an item that is not type: 1, or item-level stores sent for one that is
partner.item_combo_required400type: 2 created without components
partner.item_combo_not_allowed400combo sent for an item that is not type: 2
partner.variant_option_unknown400A SKU names an attribute or value that the body did not declare. params carries attribute and value
partner.variant_sku_incomplete400A SKU does not name every declared attribute
partner.variant_sku_duplicate400Two SKUs share a combination, or two SKUs in one update resolve to the same stored SKU
partner.variant_attribute_duplicate400The same attribute name is declared twice in variants.attributes
partner.combo_component_not_found404A component item or SKU does not exist in this merchant
partner.combo_component_invalid400A component is itself a composite item
partner.store_unknown400A storeId is not an active store of the merchant
partner.store_duplicate400The same storeId appears twice in one stores list
partner.store_not_granted403A storeId is outside the stores the merchant granted your app

The catalogue’s own codes come through unchanged, including catalogue_products.product_name_exists, catalogue_products.sku_exists, catalogue_products.barcode_exists, catalogue_products.duplicate_sku_in_request and catalogue_products.category_required. catalogue_products.variant_item_price_update_blocked is the one returned for a rate update on a variant item.

A modifier group is a question — “Milk?”, “Add extras” — with options, attached to the items that should ask it.

Terminal window
curl -s "$API_BASE_URL/modifiers" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"modifier": "Milk",
"Type": 0,
"Min": 1,
"Max": 1,
"DisplayOrder": 1,
"Options": [
{ "item": "Whole", "rate": 0, "PreDefault": true },
{ "item": "Oat", "rate": 0.40 },
{ "item": "Soy", "rate": 0.40 }
],
"ItemIds": [8841]
}'
FieldTypeRequiredNotes
modifierstringyesGroup label, up to 45 characters
Typenumberyes0 single choice, 1 multiple choice
Min / MaxnumbernoSelection bounds. Min: 1 makes the question mandatory
HasMaxnumberno1 enforces Max
IncludeQtynumberno1 lets the cashier pick a quantity per option
DisplayOrdernumbernoOrder among an item’s modifier groups
Options[].itemstringyesOption label
Options[].ratenumbernoPrice delta at the till
Options[].Rate1 / Rate2numbernoTakeaway and delivery price deltas
Options[].PreDefaultbooleannoPre-selected option
Options[].Max / QtynumbernoPer-option quantity cap and default quantity
Options[].ItemIdnumbernoLinks the option to a stocked item so it depletes inventory
ItemIdsnumber[]noItems this group is attached to

Attaching from the item’s side works too: modifierIds on POST /v1/items or PATCH /v1/items/:id sets the item’s complete list of groups.

A menu is a named selection of items with optional timings and channel flags. Use it to give delivery a different list from the dine-in till.

Terminal window
curl -s "$API_BASE_URL/menus/create-with-items" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"Menu": "All day",
"Description": "Everything except breakfast-only lines",
"Status": true,
"Online": true,
"Delivery": true,
"Takeaway": true,
"StoreIds": [1, 2],
"Timings": [{ "days": [1,2,3,4,5], "fromTime": "07:00", "toTime": "22:00" }],
"Items": [
{ "itemId": 8841, "variantId": 0 },
{ "itemId": 8842, "variantId": 0 }
]
}'
FieldTypeRequiredNotes
MenustringyesName, up to 45 characters
Descriptionstringyes
StatusbooleannoDefault true
Online, Delivery, TakeawaybooleannoChannel flags, default false
StoreIdsnumber[]noEmpty or omitted means every store
Timings[].daysnumber[]no0 Sunday through 6 Saturday
Timings[].fromTime / toTimestringnoHH:mm, store-local
Items[].itemIdnumberyes
Items[].variantIdnumberyes0 when the item has no variants; otherwise a SKU id from GET /v1/items/:id/full (variants.skus[].id)

GET /v1/menus/:id/full returns the menu with its items and timings expanded — this is the read the Online Order API uses for menu sync.

Meal slots are the slot-based builder; for a fixed bundle use a composite item (type: 2, above). Combo meals are built from slots: “pick a main”, “pick a side”, “pick a drink”.

Terminal window
# 1. Define a slot with its options
curl -s "$API_BASE_URL/combo-slots" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"name": "Choose a side",
"minSelect": 1,
"maxSelect": 1,
"allowRepeat": 0,
"options": [
{ "childItemId": 8850, "qty": 1, "isDefault": 1, "displayOrder": 1 },
{ "childItemId": 8851, "qty": 1, "extraPrice": 0.50, "displayOrder": 2 }
]
}'
# 2. Attach slots to the combo item
curl -s -X PUT "$API_BASE_URL/items/8900/meal" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "slots": [{ "slotId": 4, "displayOrder": 1 }] }'
FieldTypeRequiredNotes
namestringyesSlot label shown at the till
minSelect / maxSelectnumbernoSelection bounds for the slot
allowRepeatnumberno1 allows the same option more than once
options[].childItemIdnumberyesThe item offered in this slot
options[].childVariantIdnumberno0 when the item has no variants
options[].qtynumbernoUnits of the option included, default 1
options[].extraPricenumbernoSurcharge when this option is chosen
options[].isDefaultnumberno1 pre-selects the option
options[].displayOrdernumbernoOrder within the slot

On PUT /v1/items/:id/meal, slots attaches slot definitions (slotId, plus optional minSelect, maxSelect, displayOrder), fixed lists items always included in the combo (itemId, qty), and upsells offers paid additions triggered by a chosen item (triggerItemId, slotId, label).