Endpoints and scopes
| Request | Scope |
|---|---|
PUT /places/{place_id}/menu |
catalog:write |
PATCH /places/{place_id}/menu |
catalog:write |
DELETE /places/{place_id}/menu |
catalog:write |
GET /places/{place_id}/menu |
catalog:write |
GET /places/{place_id}/menu/hashes |
catalog:write |
POST /menus/reconcile |
catalog:write |
/places/{place_id}/products… Coming soon |
catalog:write |
Two models, not one
A menu (restaurants and cafés) is a small document read as a whole: sections, items with sizes, add-on groups with a minimum and a maximum, combo meals, time schedules. It is published and patched as one versioned unit.
Products (shops) are a large catalog of thousands of items, browsed by category and filter. Every sellable unit is a variant with its own SKU, barcode, price and stock. It is never sent as one document, but in batches.
Merging them would make menus lose their add-on rules, or shops lose per-variant stock. So each has its own model, and each place has one source: a menu or products.
The CM v1 menu
{
"schema": "aurmap.menu/1",
"currency": "SAR",
"default_language": "ar",
"languages": ["ar", "en"],
"menu_url": "https://order.partner.example/al-rayhan-olaya",
"identity": {
"name": { "ar": "مطعم الريحان", "en": "Al Rayhan" },
"tagline": { "ar": "برجر وعصائر طازجة", "en": "Burgers and fresh juice" },
"logo": "https://cdn.partner.example/logo.png",
"cover": "https://cdn.partner.example/cover.jpg",
"slider": ["https://cdn.partner.example/slide-1.jpg"],
"template": "classic",
"theme": "custom",
"colors": { "background": "#F2F4F7", "text": "#041731", "card": "#FFFFFF", "accent": "#C8102E", "on_accent": "#FFFFFF" },
"font": "Tajawal",
"button_shape": "rounded"
},
"tax": { "prices_include_vat": true, "vat_percent": 15, "service_charge_percent": 0 },
"aurmap_ordering": { "enabled": true, "pickup": true, "delivery": false, "cash": true },
"sections": [
{ "id": "burgers", "parent_id": null, "name": { "ar": "البرجر", "en": "Burgers" }, "image": null, "sort": 0, "schedule_id": null },
{ "id": "juices", "parent_id": null, "name": { "ar": "العصائر", "en": "Juices" }, "image": null, "sort": 1, "schedule_id": null },
{ "id": "desserts", "parent_id": null, "name": { "ar": "الحلى", "en": "Desserts" }, "image": null, "sort": 2, "schedule_id": "evening" },
{ "id": "meals", "parent_id": null, "name": { "ar": "الوجبات", "en": "Meals" }, "image": null, "sort": 3, "schedule_id": null }
],
"items": [
{
"id": "chicken-burger",
"type": "menu_item",
"section_id": "burgers",
"sort": 0,
"name": { "ar": "برجر دجاج", "en": "Chicken burger" },
"description": { "ar": "صدر دجاج مقرمش، خس، صوص البيت", "en": "Crispy chicken breast, lettuce, house sauce" },
"image": "https://cdn.partner.example/img/chicken-burger.jpg",
"price_minor": 2300,
"original_price_minor": null,
"calories": 540,
"prep_min": 10,
"prep_max": 15,
"label": { "name": { "ar": "الأكثر طلبا", "en": "Best seller" } },
"warnings": [ { "id": "gluten", "name": { "ar": "يحتوي على الغلوتين", "en": "Contains gluten" }, "icon": null } ],
"sizes": [
{ "id": "m", "name": { "ar": "وسط", "en": "Medium" }, "price_minor": 2300, "original_price_minor": null, "calories": 540 },
{ "id": "l", "name": { "ar": "كبير", "en": "Large" }, "price_minor": 2990, "original_price_minor": null, "calories": 720 }
],
"group_ids": ["sauce", "extras"],
"order_types": { "pickup": true, "delivery": true, "dine_in": true },
"recommended_ids": ["orange-juice"],
"schedule_id": null,
"order_url": null
},
{
"id": "orange-juice",
"type": "menu_item",
"section_id": "juices",
"sort": 1,
"name": { "ar": "عصير برتقال", "en": "Orange juice" },
"price_minor": 920,
"sizes": [
{ "id": "s", "name": { "ar": "صغير", "en": "Small" }, "price_minor": 920 },
{ "id": "l", "name": { "ar": "كبير", "en": "Large" }, "price_minor": 1380 }
],
"group_ids": [],
"order_types": { "pickup": true, "delivery": true, "dine_in": true }
},
{
"id": "kunafa",
"type": "menu_item",
"section_id": "desserts",
"sort": 2,
"name": { "ar": "كنافة", "en": "Kunafa" },
"price_minor": 1840,
"sizes": [],
"group_ids": [],
"order_types": { "pickup": true, "delivery": true, "dine_in": true }
},
{
"id": "chicken-burger-meal",
"type": "menu_item",
"section_id": "meals",
"sort": 3,
"name": { "ar": "وجبة برجر دجاج", "en": "Chicken burger meal" },
"price_minor": 3000,
"original_price_minor": 3220,
"sizes": [],
"group_ids": ["sauce"],
"order_types": { "pickup": true, "delivery": true, "dine_in": true }
}
],
"groups": [
{
"id": "sauce", "name": { "ar": "الصوص", "en": "Sauce" }, "required": true, "min": 1, "max": 1,
"options": [
{ "id": "hot", "name": { "ar": "صوص حار", "en": "Hot sauce" }, "price_minor": 230, "image": null },
{ "id": "none", "name": { "ar": "بدون صوص", "en": "No sauce" }, "price_minor": 0, "image": null }
]
},
{
"id": "extras", "name": { "ar": "إضافات", "en": "Extras" }, "required": false, "min": 0, "max": 2,
"options": [
{ "id": "cheese", "name": { "ar": "جبن إضافي", "en": "Extra cheese" }, "price_minor": 345, "image": null },
{ "id": "no-onion", "name": { "ar": "بدون بصل", "en": "No onion" }, "price_minor": 0, "image": null },
{ "id": "pickles", "name": { "ar": "مخلل إضافي", "en": "Extra pickles" }, "price_minor": 0, "image": null }
]
}
],
"combos": [
{ "item_id": "chicken-burger-meal", "components": [ { "item_id": "chicken-burger", "quantity": 1 }, { "item_id": "orange-juice", "quantity": 1 } ] }
],
"schedules": [
{ "id": "evening", "days": [0, 1, 2, 3, 4, 5, 6], "from": "16:00", "to": "02:00" }
],
"state": {
"paused": false,
"sold_out": { "items": ["kunafa"], "sizes": [], "options": [] },
"prices": { "items": [], "sizes": [], "options": [] }
}
}All amounts are in halalas: 2990 means 29.90 in riyals. Every text is a language map.
Top-level fields
| Field | Description |
|---|---|
schema |
aurmap.menu/1; optional |
currency |
currency code; default SAR |
default_language, languages |
the base language and every language used; default ar |
menu_url |
an https link to order from your system, shown when ordering on AurMap is off |
identity |
the menu’s look; optional, the place’s identity on AurMap is used without it |
tax |
prices_include_vat, vat_percent, service_charge_percent, as on the merchant’s menu |
aurmap_ordering |
what your menu can take: enabled, pickup, delivery, cash |
sections |
sections |
items |
items; required |
groups |
add-on groups, each defined once |
combos |
combo meals |
schedules |
times when a section or item can be ordered |
state |
state: pause, sold out, temporary prices; kept apart from the structure |
Do not send version, hash or per-item hashes. We compute them and return them to you.
Identity
| Field | Values |
|---|---|
name, tagline |
language maps |
logo, cover, slider[] |
https image links |
template |
classic, editorial, darkluxe or delivery |
theme |
light, dark or custom |
colors |
with custom: background, text, card, accent, on_accent as #RRGGBB |
font |
font name |
button_shape |
rounded, pill or square |
Sections
| Field | Description |
|---|---|
id |
your section id |
parent_id |
the parent of a sub-section, or null |
name, image, sort |
name, image and order |
schedule_id |
a schedule that limits when the section can be ordered, or null |
An item names its section in section_id. A section does not list its items.
Items
| Field | Description |
|---|---|
id |
your stable item id, up to 64 chars of [A-Za-z0-9_.-]; returned to you in orders as is |
product_id |
your system’s internal product number if it differs from id; optional |
type |
menu_item, product or service |
section_id, sort |
section and order |
name, description, image |
name, description, image |
price_minor |
the price; with sizes, the lowest size price, for display only |
original_price_minor |
the price before a discount, shown struck through |
calories, prep_min, prep_max |
calories, and preparation time in minutes |
label |
a badge such as “Best seller”: { "name": {…} } |
warnings[] |
notices such as allergens: id, name, icon |
sizes[] |
sizes; see the next table |
group_ids[] |
the add-on groups that apply |
order_types |
pickup, delivery, dine_in |
recommended_ids[] |
items we suggest alongside |
schedule_id |
the item’s own schedule, or null |
order_url |
an order link for this item; optional |
Sizes
| Field | Description |
|---|---|
id |
your size id |
name |
name |
price_minor |
the item’s whole price in this size, not a difference |
original_price_minor, calories |
optional |
Add-on groups
| Field | Description |
|---|---|
id, name |
id and name |
required |
whether a choice is required |
min, max |
the fewest and most options; max of 0 means no limit |
options[] |
id, name, price_minor (added to the line), image |
Combos
item_id is the combo’s own item, with its own price in items. components[] are its parts: item_id and quantity.
Schedules
| Field | Description |
|---|---|
id |
schedule id |
days[] |
weekdays as numbers: 0 Sunday, 1 Monday, up to 6 Saturday |
from, to |
HH:MM in the place’s time zone |
When to is earlier than from, the span runs past midnight and belongs to the day it starts on. Example: days: [4] from 16:00 to 02:00 means Thursday 4 pm to Friday 2 am.
State
| Field | Description |
|---|---|
paused |
true stops ordering from the whole menu |
sold_out |
items[] item ids, sizes[] of item_id and size_id, options[] of group_id and option_id |
prices |
temporary prices over the structure’s prices: items[], sizes[] and options[], each with price_minor |
Updating state alone is on availability.
Rules
- Every referenced id must exist: the section, groups, schedules, recommended items, combo parts and everything in the state.
required: truemeansminis one or more.maxis either0or not belowmin.- Days 0 to 6, times
00:00to23:59, andfromdiffers fromto. - Every id of yours, for items, sizes, groups, options, sections and schedules, is up to 64 chars of
[A-Za-z0-9_.-]. The colon:is refused because we reserve it. - Every link must be
https, on a public internet address. - Limits: 2 MB, 3,000 items, 300 groups, 50 options per group.
Any violation answers 422 MENU_INVALID with the location of each problem:
{
"error": {
"code": "MENU_INVALID",
"message": "The menu has 2 problems.",
"request_id": "req_01JAA2",
"details": [
{ "path": "items[0].section_id", "code": "UNKNOWN_SECTION", "message": "Section breakfast does not exist." },
{ "path": "groups[1]", "code": "MIN_ABOVE_MAX", "message": "min is above max." }
]
}
}Publish the full menu
PUT /places/{place_id}/menu with catalog:write.
IDEMPOTENCY_KEY="menu-olaya-2026-10-14-01" \
aurmap PUT /v1/places/plc_9q3w7e/menu "$(cat menu.json)"202
{ "place_id": "plc_9q3w7e", "version": 42, "prev_version": 41, "kind": "full", "status": "processing", "hash": "9f2c4be1d07a65e3c1f8a2b4d6e8f0a1b3c5d7e9f1a3b5c7d9e1f3a5b7c9d1e3", "warnings": [] }- We validate the format at once, then prepare the images and publish within seconds, and you receive
menu.changedwith the same version and hash. - An image we cannot fetch never fails the publish. We skip it and list it in
warnings. - If you send a menu identical to the current one, we answer
200with"status": "unchanged"and create no version. - To avoid overwriting a change you have not seen, send
If-Match: "41". If the current version is not 41 we answer409 VERSION_CONFLICT.
import { readFile } from 'node:fs/promises';
const menu = JSON.parse(await readFile('menu.json', 'utf8'));
const res = await aurmap('PUT', '/v1/places/plc_9q3w7e/menu', menu, 'menu-olaya-2026-10-14-01');
console.log(res.version, res.hash);$menu = json_decode(file_get_contents('menu.json'), true);
$res = aurmap('PUT', '/v1/places/plc_9q3w7e/menu', $menu, 'menu-olaya-2026-10-14-01');
echo $res['version'], ' ', $res['hash'];Patch the structure
PATCH /places/{place_id}/menu with catalog:write.
aurmap PATCH /v1/places/plc_9q3w7e/menu '{"base_version":43,"upsert":[{"id":"mint-lemonade","type":"menu_item","section_id":"juices","sort":4,"name":{"ar":"ليموناضة بالنعناع","en":"Mint lemonade"},"price_minor":1150,"sizes":[],"group_ids":[],"order_types":{"pickup":true,"delivery":true,"dine_in":true}}],"remove":["kunafa"],"groups":[{"id":"extras","name":{"ar":"إضافات","en":"Extras"},"required":false,"min":0,"max":3,"options":[{"id":"cheese","name":{"ar":"جبن إضافي","en":"Extra cheese"},"price_minor":345,"image":null},{"id":"no-onion","name":{"ar":"بدون بصل","en":"No onion"},"price_minor":0,"image":null},{"id":"pickles","name":{"ar":"مخلل إضافي","en":"Extra pickles"},"price_minor":0,"image":null},{"id":"jalapeno","name":{"ar":"هالبينو","en":"Jalapeno"},"price_minor":230,"image":null}]}]}'{ "place_id": "plc_9q3w7e", "version": 44, "prev_version": 43, "kind": "structure", "status": "processing", "hash": "b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2", "warnings": [] }| Field | Description |
|---|---|
base_version |
the version your change is based on; required |
upsert[] |
whole items; an existing item is replaced whole |
remove[] |
item ids to delete |
groups[] |
whole groups, added or replaced |
remove_groups[] |
group ids to delete |
sections[], combos[], schedules[] |
each one sent replaces the whole list |
identity, tax, aurmap_ordering |
each one sent replaces the whole value |
state |
state, under the availability rules |
If the menu changed after base_version, we answer 409 VERSION_CONFLICT with current_version. Read the menu or publish it in full.
Remove the menu
DELETE /places/{place_id}/menu takes your menu off the place, and you receive menu.removed. After that, GET /places/{place_id}/menu answers 404 MENU_NOT_FOUND.
Versions and hashes
| Request | Returns |
|---|---|
GET /places/{place_id}/menu |
the current version, its hash, publish time, and the full menu with state and per-item hashes |
GET /places/{place_id}/menu?version=42 |
an earlier version while it is kept; otherwise 404 MENU_VERSION_GONE |
GET /places/{place_id}/menu/hashes |
the menu hash and one hash per item |
POST /menus/reconcile |
compares what you hold with what we hold, for many places at once |
versiongrows by one with every accepted change: a full publish, a structure patch or a state update.hashis a 64-character string. Compare it for equality only.
Once a night, send the last version and hash you know for each place, up to 500 places per request:
aurmap POST /v1/menus/reconcile '{"places":[{"place_id":"plc_9q3w7e","version":44,"hash":"b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2"}]}'{ "stale": [] }For every place listed in stale, read its menu or publish it in full again.
Images
- We fetch each image once and keep it in several sizes.
- We fetch only from public
httpsaddresses. Private and local addresses, such as10.x,192.168.x,127.0.0.1and169.254.x, are refused, and redirects are not followed. A refused image is skipped with warningIMAGE_URL_NOT_ALLOWED. - To change an image, change its address.
- Up to 8 MB, in JPEG, PNG or WebP.
Products for shops (CP v1) Coming soon
The product model for shops and e-commerce platforms. Each product has option axes (size, colour) and variants, each with its own SKU, price and stock.
{
"schema": "cp/1",
"id": "tshirt-basic",
"name": { "ar": "تيشيرت قطن", "en": "Cotton T-shirt" },
"category_ids": ["men", "tshirts"],
"images": [ { "url": "https://cdn.partner.example/p/tshirt.jpg" } ],
"option_axes": [
{ "id": "size", "name": { "ar": "المقاس", "en": "Size" }, "values": [ { "id": "m", "name": { "ar": "M", "en": "M" } }, { "id": "l", "name": { "ar": "L", "en": "L" } } ] }
],
"variants": [
{ "id": "tshirt-basic-m", "sku": "TS-M-BLK", "barcode": "6281234567890", "options": { "size": "m" }, "price_minor": 7900, "stock": { "tracked": true, "quantity": 12 } },
{ "id": "tshirt-basic-l", "sku": "TS-L-BLK", "options": { "size": "l" }, "price_minor": 7900, "stock": { "tracked": true, "quantity": 0 } }
],
"status": "active"
}| Request | Description |
|---|---|
PUT /places/{place_id}/products/{product_id} |
add or replace a product |
POST /places/{place_id}/products/batch |
up to 500 products per call, with a result per product |
DELETE /places/{place_id}/products/{product_id} |
delete a product |
GET /places/{place_id}/products |
products, paged |
PUT /places/{place_id}/product-categories |
the category tree |
GET /places/{place_id}/products/hashes |
a hash per product, for reconciling |