ala.menu for developers
Connect your POS to ala.menu
ala.menu hosts the digital menu your customers open on their phone. This API keeps that menu in step with the system you already run. Your POS or back office stays the source of truth for prices and stock. You push what changed to one endpoint, and the menu updates within seconds. You cannot delete anything and you cannot break how the menu looks: the API only accepts facts about items, and everything about presentation is managed on the ala.menu side.
The model
How it works
- 1
You get a key
We issue one secret key per menu. Send it on every request as
Authorization: Bearer pos_YOURMENU_xxxx. Keep it on your server. If it leaks, tell us and we issue a new one. - 2
You send items by their own id
Every item carries an
externalId: a barcode, or any string from your system that identifies the item and never changes. OneexternalIdis one item on the menu. You never need to know our item ids. - 3
You send only what changed, whenever it changes
A price change is one small call with one item and one field. Fields you leave out are left alone. Items you leave out are left alone. Wire this to whatever event your system already fires.
- 4
On a schedule you choose, you send everything
One call with your entire active catalog. It is the safety net. Anything a delta missed, dropped or got wrong corrects itself at the next full sync. The gap between full syncs is the longest a missed change can stay wrong, so pick the window you can live with. Most integrations run it daily, and a quiet hour works well.
Ten minutes
Quickstart
Three calls. Replace pos_YOURMENU_xxxx with the key we gave you and paste each block into a terminal.
1. Check the key
curl https://ala.menu/api/pos/v1/ping \
-H "Authorization: Bearer pos_YOURMENU_xxxx"{
"ok": true,
"menu": "Pegasus Pet Center",
"currency": "USD"
}If the menu name and the currency are the ones you expect, the key is in the right environment.
2. See what we already hold
curl https://ala.menu/api/pos/v1/catalog \
-H "Authorization: Bearer pos_YOURMENU_xxxx"{
"ok": true,
"currency": "USD",
"items": [
{
"externalId": "9003579311776",
"name": "RC Wet Sterilised Jelly 85g",
"price": 4.5,
"inStock": true,
"hidden": false,
"hasImage": true,
"brand": "Royal Canin",
"size": "85g",
"barcode": "9003579311776"
},
{
"externalId": "9003579311714",
"name": "RC Kitten Dry 2kg",
"price": 28,
"inStock": false,
"hidden": false,
"hasImage": false,
"brand": "Royal Canin",
"size": "2kg"
}
]
}This is the menu as customers see it, listed by your own ids. Use it to check that your ids match ours before you write anything.
3. Send your first change
One price change and one item going out of stock, in one call.
curl -X POST https://ala.menu/api/pos/v1/sync \
-H "Authorization: Bearer pos_YOURMENU_xxxx" \
-H "Content-Type: application/json" \
-d '{
"mode": "delta",
"currency": "USD",
"items": [
{ "externalId": "9003579311776", "price": 5.0 },
{ "externalId": "9003579311714", "quantity": 0 }
]
}'{
"ok": true,
"received": 2,
"updated": 2,
"created": 0,
"staged": 0,
"unchanged": 0,
"hidden": 0,
"unhidden": 0,
"rejected": []
}Open the menu and the new price is there. Run the same call again and the response comes back with unchanged: 2, because nothing moved the second time.
Reference
Endpoints
Base URL https://ala.menu/api/pos/v1. Every endpoint needs the same header: Authorization: Bearer pos_YOURMENU_xxxx. Responses are JSON.
Endpoint
Check the key#
Confirms your key works and tells you which menu it is tied to.
ok, menu and currency. HTTP 200.{
"ok": true,
"menu": "Pegasus Pet Center",
"currency": "USD"
}Errors: the shared ones below. There is nothing else this call can get wrong.
Endpoint
Read what we hold#
Returns every item on the menu that is linked to one of your external ids, in the state customers see it.
currency and an items array, one row per linked item. HTTP 200.externalId is not the barcode.{
"ok": true,
"currency": "USD",
"items": [
{
"externalId": "9003579311776",
"name": "RC Wet Sterilised Jelly 85g",
"price": 4.5,
"inStock": true,
"hidden": false,
"hasImage": true,
"brand": "Royal Canin",
"size": "85g",
"barcode": "9003579311776"
},
{
"externalId": "9003579311714",
"name": "RC Kitten Dry 2kg",
"price": 28,
"inStock": false,
"hidden": false,
"hasImage": false,
"brand": "Royal Canin",
"size": "2kg"
}
]
}One thing to note. Items the menu owner created by hand are not in this list, because they are not linked to your system.
The retail facets come back here too, so anything you push you can read back and check against your own records. When your externalId is not the barcode, barcode is the column to match your rows on.
Errors: the shared ones below.
Endpoint
Push changes#
Sends a batch of items. This is the endpoint the whole integration hangs off, and it has two modes.
mode, currency and items (up to 500 per delta call, up to 25,000 in a full sync).Request fields
delta when you leave it out.USD. It must match the menu currency or the whole call is rejected. This is what stops a price list in the wrong currency from landing.Item fields
Only externalId is required. Everything else is optional, and leaving a field out means "do not change this".
externalId.externalId.idle takes the item off the menu without deleting it. active puts it back, whatever the reason it was hidden.{ "id": "17", "name": "Dog Food" }, using your own category id. An id we have not seen creates a new menu section. Send null to clear the category and drop the item into a fallback section.null to clear the photo we hold. To upload a file instead, use the image endpoint below.null to clear it.500g or 6 x 1L. Up to 200 characters, trimmed. Send null to clear it.externalId. Send null to clear it.mode: "delta", the everyday call
When to use it: every time something changes in your system. A price edit, a stock movement, a new product, a discontinued line. This is the normal path and it is what makes the menu update within seconds.
A delta carries only the items that changed, and inside each item only the fields that changed. Items you do not mention are untouched.
{
"mode": "delta",
"currency": "USD",
"items": [
{ "externalId": "9003579311776", "price": 5.0 },
{ "externalId": "9003579311714", "quantity": 0 },
{ "externalId": "9003579311888", "status": "idle" },
{
"externalId": "6191544800014",
"name": "New Product",
"price": 12.0,
"quantity": 20,
"category": { "id": "17", "name": "Dog Food" },
"brand": "Royal Canin",
"size": "2kg"
}
]
}{
"ok": true,
"received": 4,
"updated": 3,
"created": 1,
"staged": 0,
"unchanged": 0,
"hidden": 1,
"unhidden": 0,
"rejected": []
}Three existing items moved (a price, a stock count, one going idle) and one unknown id created a new item. The idle one counts in both updated and hidden, because it changed and it left the menu in the same call.
mode: "full", the full sync
When to use it: on a schedule you choose, carrying your entire active catalog. How often is your call: the gap between full syncs is the longest anything a delta missed can stay wrong. Most integrations run it daily. It is a safety net, not a way to make routine updates. Do not use it to push a single price change.
Same payload shape. We reconcile everything against it: values that drifted are corrected, and linked items that are not in the snapshot are hidden. Nothing is deleted, and an item that shows up again in a later snapshot comes back on its own.
{
"mode": "full",
"currency": "USD",
"items": [
{
"externalId": "9003579311776",
"name": "RC Wet Sterilised Jelly 85g",
"price": 5.0,
"quantity": 42,
"category": { "id": "17", "name": "Dog Food" }
},
{
"externalId": "9003579311714",
"name": "RC Kitten Dry 2kg",
"price": 28.0,
"quantity": 0,
"category": { "id": "18", "name": "Cat Food" }
}
]
}A full snapshot that leaves out too much of the catalog is refused. See suspect_snapshot in the errors below.
Response counters
The response tells you exactly what happened. Every call returns all eight fields, including the zeros.
items array, rejected ones included.status: "idle". They exist and wait to be released.status: "active", or a full snapshot included an item that an earlier snapshot had missed.{ externalId, reason }. Rows we could not apply. The rest of the batch still landed. Usually empty.created, updated, staged, unchanged and the length of rejected add up to received. hidden and unhidden count something else: they overlap with updated, and a full snapshot can hide items you did not mention at all.
Two kinds of failure
Some problems are about one row, and some are about the whole payload. They are answered differently on purpose.
One row is wrong. You get HTTP 200. The rest of the batch lands and the bad rows come back in rejected. A single malformed product must never strand a full sync.
{
"ok": true,
"received": 3,
"updated": 2,
"created": 0,
"staged": 0,
"unchanged": 0,
"hidden": 0,
"unhidden": 0,
"rejected": [
{ "externalId": "6191544800014", "reason": "missing_price" }
]
}The payload is wrong. You get HTTP 422 and nothing at all is written. Accepting half of a payload with the wrong currency in it would be worse than accepting none of it.
{ "ok": false, "error": "currency_mismatch" }Whole-call errors, HTTP 422
Nothing was written. Fix the payload and resend.
mode was something other than delta or full./ping to see which one the menu uses.items was not an array.items was an empty array. Send at least one item.externalId appeared twice in one payload. Which row wins would be a coin toss, so neither does. Merge them and resend.Per-item reasons, inside rejected
These come back with HTTP 200. The rest of the batch landed.
externalId, since there was no usable one.name or price as null. Those two can never be cleared. To say nothing about a field, leave it out.active or idle.externalId arrived without a name. Creating an item needs a name and a price.externalId arrived without a price. Same rule as above.Endpoint
Upload an item photo#
Sends one photo for one item. The image file is the raw request body, not a form field and not JSON.
externalId in the path, an image Content-Type, and the file itself as the body.imageUrl and applied. HTTP 200./sync. The item has to exist already, so create it with a sync call first.image/jpeg or image/png. We also read the first bytes of the file, so a header that disagrees with the file is refused.curl -X POST https://ala.menu/api/pos/v1/items/9003579311776/image \
-H "Authorization: Bearer pos_YOURMENU_xxxx" \
-H "Content-Type: image/jpeg" \
--data-binary @product.jpg{
"ok": true,
"imageUrl": "https://firebasestorage.googleapis.com/...jpg?alt=media&token=...",
"applied": true
}We resize the photo to fit within 1600 by 1600 and store it as a JPEG at its own permanent URL. Nothing is ever overwritten, so a new upload is always a new URL.
applied tells you whether your photo is the one customers now see. It comes back false when the menu owner has set their own photo for that item: yours is stored, theirs keeps showing. That is not an error and there is nothing to retry.
Shared
Errors every endpoint can return#
Errors always come back as { "ok": false, "error": "..." }. The error string is stable, so it is safe to log it and to branch on it.
Guarantees
Rules that keep you safe
These are the reasons you can wire this up without a rollback plan. None of them depend on your code being careful.
A field you leave out changes nothing
Absent means "no change", never "clear this". Sending only
externalIdandpricetouches the price and nothing else.An explicit null clears the field
Only
category,imageUrl,brand,sizeandbarcodecan be cleared that way. A nullnameorpriceis a bug in the caller, not an instruction, so that row is rejected and the item keeps what it had.Resending is always harmless
Every call is idempotent. If a request times out and you do not know whether it landed, send it again. A repeat that changes nothing counts as
unchangedand does not even touch the menu.Nothing is ever deleted
There is no delete in this API. An item you stop sending, or mark idle, is hidden. Its history, its photos and the owner edits on it all stay. Include it in a full sync again and it comes back on its own.
A broken export cannot wipe the menu
A full snapshot missing a large share of the catalog is refused as
suspect_snapshotand changes nothing. A half-written export file cannot empty a menu.Quantity zero sells out, above zero brings back
Stock is a fact you own. Zero shows the item as sold out and blocks it at checkout, anything above zero restores it. If you never send
quantity, we leave the sold-out switch the owner set by hand exactly as it is.Presentation is not yours to break
Layout, descriptions, section order and design are managed on the menu side. Your calls cannot reorder, rename or remove anything you were not talking about. If an owner has edited a name, a price or a photo by hand, that edit stays on top and your value is kept underneath.
Questions
FAQ
What if the internet at the shop dies?
Queue the changes and send them when the connection is back. Resending is safe, and order does not matter much because the last value wins. If you would rather not build a queue, do nothing: your next full sync fixes everything the outage lost.
What if I send the same thing twice?
Nothing happens the second time. The response comes back with the item counted as unchanged, and the menu is not touched at all.
Can I break the menu design?
No. You send facts about items. Layout, fonts, photos, descriptions and section order are managed on the menu side and your calls never reach them.
What happens to new items?
An externalId we have not seen, sent with at least a name and a price, creates the item. Include a category and it lands in the matching section, or creates that section if it is new. No category lands it in a fallback section. Some owners ask us to hold new items for review, in which case the item is created but waits off the menu and the response counts it as staged.
What about item photos?
Two ways. Send an imageUrl in a sync call if you already host the file, or POST the file itself to the image endpoint. A photo the owner set by hand always keeps showing.
What currency do I send?
The menu currency, on every call, as a three-letter code. Call /ping to see which one it is. Prices go in that currency as plain numbers, not in cents. A mismatch rejects the whole call rather than repricing anything.
How fast do changes appear?
Seconds. Send a price change and the next customer to open the menu sees it.
Are there rate limits?
120 requests a minute per menu, shared across all four endpoints. Up to 500 items in a delta call, and up to 25,000 in a full sync so the whole catalog always fits in one call. If many things change at once, one call with all of them beats many small calls.
OpenAPI file
The whole API as a machine-readable spec. Import it into Postman, Insomnia, or a client generator.
Download openapi.yamlQuestions
Stuck on a payload, or want a key for a test menu? WhatsApp us.
ala.menu Catalog Sync API, version 1. Base URL https://ala.menu/api/pos/v1.