For developers
Angie Eats Partner API
Read a restaurant's menu and report items sold out, from a POS till, an aggregator, or any other outside system acting on Angie Eats's behalf. Three endpoints today, deliberately a small first surface.
Base URL
eats.heyangie.ai/api/v1
Auth
Bearer API key
Format
JSON
Getting a key, and what to do when it leaks
There is no self-service signup. An Angie Eats engineer mints your key on a partner record naming your company, a technical contact, the market you're cleared to trade in, and — for a single-chain integration — the specific restaurants you may reach. Email partners@eats.heyangie.ai with your company name, the market, and the narrowest set of scopes that does the job (see Scopes).
You'll be sent the plaintext key exactly once. The platform stores only a SHA-256 hash of it — there is no "reveal key" screen, because there is nothing to reveal. If you lose it, ask for a new one; the old one gets revoked.
If a key ever turns up somewhere it shouldn't — a public repository, a client-side bundle, a log aggregator you don't control — email us to revoke it, quoting the key's prefix (the part before the final segment, e.g. zpk_live_cctOfhbC_). The prefix is stored in the clear specifically so a key can be identified without being usable. Revocation takes effect on the very next request — ask for a replacement at the same time so your integration has something to roll onto.
Authentication
Send the key as a bearer token:
Authorization: Bearer zpk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxor, if a custom header is easier for your HTTP client than Authorization:
X-Api-Key: zpk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxBoth were verified to work identically against the live API.
The key is never accepted as a query parameter — this is not an oversight, the middleware only reads the two headers above. A key in a URL ends up in server access logs, browser history, and any Referer header sent to a third party. Verified live: a request with ?api_key=… instead of a header returns 401, the same as sending no key at all.
Scopes
Every key is granted one or more scopes, and every route requires exactly one. A key missing the scope a route needs gets a 403 that names the scope — see Errors. Read and write are always separate scopes: the commonest integration is a menu sync that only ever reads, and the second commonest is a till that only ever writes availability. Granting both because it's convenient turns a single leaked key into fraudulent menu edits, not just a data leak.
| Scope | Grants | Used today |
|---|---|---|
menu:read | Read restaurants, opening hours and menus. | Yes |
menu:write | Change prices, descriptions and whether a dish is sold out. | Yes |
orders:write | Place orders. | Not yet — no endpoint |
orders:read | Read orders and their current status. | Not yet — no endpoint |
orders:advance | Move an order through the kitchen — accept, ready, completed. | Not yet — no endpoint |
delivery:read | Read delivery areas, fees and whether an address is covered. | Not yet — no endpoint |
webhooks:manage | Create and remove webhook subscriptions. | Not yet — no endpoint |
Endpoints
All three live under /api/v1/partner and require a valid key with the scope noted.
/partner/restaurantsmenu:readLists the restaurants your key can see — filtered to your partner's market, and further to a specific list if your grant is confined to one. Inactive restaurants are excluded. Capped at 500 results; there is no pagination yet.
curl -s https://eats.heyangie.ai/api/v1/partner/restaurants \
-H "Authorization: Bearer $ANGIE_EATS_KEY"{
"restaurants": [
{
"id": 25,
"slug": "demo-kitchen",
"name": "Demo Kitchen",
"market": "US",
"currency": "USD",
"timezone": "America/Chicago",
"accepts_orders": true
},
{
"id": 26,
"slug": "sakura-sushi-25435",
"name": "Sakura Sushi 25435",
"market": "US",
"currency": "USD",
"timezone": "America/Chicago",
"accepts_orders": false
}
]
}accepts_orders: false means the restaurant is visible but not currently taking orders through Angie Eats — worth showing, not worth routing an order to.
/partner/restaurants/{restaurant}/menumenu:readThe full menu: sections, items, sizes, and modifier groups with their modifiers. Every price is an integer minor unit — see Money below.
curl -s https://eats.heyangie.ai/api/v1/partner/restaurants/25/menu \
-H "Authorization: Bearer $ANGIE_EATS_KEY"{
"restaurant": {
"id": 25,
"slug": "demo-kitchen",
"name": "Demo Kitchen",
"currency": "USD",
"currency_minor_units": 2,
"timezone": "America/Chicago"
},
"menu": [
{
"id": 3,
"name": "Pizza",
"items": [
{
"id": 5,
"name": "Margherita",
"description": "San Marzano, fior di latte, basil.",
"price": 1295,
"is_sold_out": false,
"sizes": [
{ "id": 1, "name": "10\"", "price": 1295 },
{ "id": 2, "name": "12\"", "price": 1695 }
],
"modifier_groups": [
{
"id": 6,
"name": "Choose a base",
"selection_mode": "single",
"min_selections": 1,
"max_selections": 1,
"is_required": true,
"modifiers": [
{ "id": 14, "name": "Classic", "price_delta": 0 },
{ "id": 12, "name": "Thin crust", "price_delta": 500 }
]
}
]
}
]
}
]
}price_delta on a modifier is already the modifier group's decision — never re-derive it elsewhere. A restaurant outside your grant returns 404, not 403; see Errors.
/partner/restaurants/{restaurant}/availabilitymenu:writeMarks one or more dishes sold out, or brings them back. The endpoint a till actually wants: report you've run out of lamb before the next customer orders it.
curl -s -X POST https://eats.heyangie.ai/api/v1/partner/restaurants/25/availability \
-H "Authorization: Bearer $ANGIE_EATS_KEY" \
-H "Content-Type: application/json" \
-d '{"items":[{"id":39,"is_sold_out":true},{"id":999999,"is_sold_out":true}]}'{ "updated": 1, "ignored": [999999] }This response is real — one item was genuinely applied, and 999999doesn't exist. A stale id is reported, not fatal: syncing 400 items with one bad id still applies the other 399. Always check `ignored`, even on a 200.
Errors
Every refusal is a JSON body shaped {"message": "..."} (422 additionally carries an errors object keyed by field). This table is exhaustive for the partner surface today.
| Status | Meaning | Retry? |
|---|---|---|
| 401 | No key, a wrong key, a revoked key, or an expired key. The last three return the identical message on purpose. | No |
| 403 | Missing scope ("This key does not have the … permission.") or a suspended partner account — two different messages, one status. | No |
| 404 | The restaurant doesn't exist, or exists but is outside your grant. Both look identical. | No |
| 422 | Request body failed validation. | No |
| 429 | This key's per-minute ceiling was reached. Carries Retry-After. | Yes |
A wrong key, a revoked key, and an expired key look identical
On purpose. If a revoked key said "revoked" and a wrong key said "invalid", anyone probing with random strings could tell which guesses were real keys merely turned off. All three get the same message — captured live:
$ curl -s .../partner/restaurants -H "Authorization: Bearer zpk_live_wrong..."
{"message":"That API key is not valid."}
$ curl -s .../partner/restaurants -H "Authorization: Bearer $A_REVOKED_KEY"
{"message":"That API key is not valid."}
$ curl -s .../partner/restaurants -H "Authorization: Bearer $AN_EXPIRED_KEY"
{"message":"That API key is not valid."}"Outside your grant" and "doesn't exist" both return 404
A restaurant that exists but isn't in your grant is indistinguishable from one that was never real — otherwise a partner could enumerate the platform's full restaurant list by watching which ids return 403 versus 404.
Rate limiting
Each key has its own ceiling — 60 requests per minute by default, keyed on the credential, not your IP or your partner account. A busy integration behind one NAT gateway isn't throttled as one caller, and a runaway retry loop in staging can't burn through your production ceiling — they're different keys. Ask Angie Eats for a higher ceiling up front if your traffic profile needs one.
HTTP/1.1 429 Too Many Requests
Retry-After: 59
{"message":"Rate limit of 3 requests a minute reached."}Back off using Retry-After, in seconds — don't guess, and don't retry immediately. A 429 is a ceiling, not a fault; hammering it afterward only pushes your reset further out. If you're hitting 429s in normal operation rather than during a burst (an initial full-catalogue sync, say), ask for a higher per-key ceiling rather than building more elaborate retry logic.
Money: integer minor units
Every price — price, sizes[].price, and price_delta — is an integer number of the currency's smallest unit, alongside currency (ISO 4217) and currency_minor_units (how many decimal places that currency uses, 0–4, carried on every response so you never hardcode it).
1200 is not $1200. For a restaurant with "currency": "USD", "currency_minor_units": 2, the item priced 1200 costs $12.00 — 1200 cents, divided by 10². The exponent comes from currency_minor_units, not from an assumption that every currency uses two decimal places: a zero-decimal currency would carry currency_minor_units: 0, where 1200 means 1200 units exactly, not 12.00.
Do not divide by 100 and prepend a currency symbol — that's correct for USD by accident and wrong the day a market with a different currency is added. The correct rendering:
displayed = raw_price / (10 ** currency_minor_units)then format displayed with your own platform's currency formatter (in JavaScript, Intl.NumberFormatwith the currency code) — only your side knows the reader's locale.
Versioning
The current and only version is v1, in the URL path. Within it, Angie Eats commits to:
- No field is removed and no field changes meaning without a new version.
- New fields may be added to any response at any time — your integration must ignore fields it doesn't recognize.
- New endpoints and new scopes may be added without a version bump.
- A breaking change ships as
v2, withv1kept running for a publicized deprecation window. There is nov2today.
Your first integration in ten minutes
1. Get a key.
Email partners@eats.heyangie.ai with your market and
menu:read(andmenu:writeif you'll report sold-out items). Save thezpk_live_…string somewhere real before you close that message — it won't be shown again.2. List what you can see.
Terminalcurl -s https://eats.heyangie.ai/api/v1/partner/restaurants \ -H "Authorization: Bearer $ANGIE_EATS_KEY"A 401 usually means the header is missing its space (
Bearer <key>). An emptyrestaurantsarray on a 200 usually means your grant is scoped to a list that doesn't include anything active — ask Angie Eats to confirm it.3. Pull one restaurant's menu.
Terminalcurl -s https://eats.heyangie.ai/api/v1/partner/restaurants/25/menu \ -H "Authorization: Bearer $ANGIE_EATS_KEY"Render one price correctly before writing anything else — see Money first.
4. If you have
menu:write, mark one item sold out and confirm it sticks.Terminalcurl -s -X POST https://eats.heyangie.ai/api/v1/partner/restaurants/25/availability \ -H "Authorization: Bearer $ANGIE_EATS_KEY" -H "Content-Type: application/json" \ -d '{"items":[{"id":<item id from step 3>,"is_sold_out":true}]}'Check
updatedmatches what you sent andignoredis empty, then re-fetch the menu to confirmis_sold_out: true. Flip it back when you're done testing.5. Handle the two things that happen in production and never in a demo.
A 429 — back off using
Retry-After, never hot-loop. A non-emptyignoredon a bulk write — log it, it means your catalogue and Angie Eats's have drifted on at least one item, not that the call failed.
Not yet available
The scopes below are defined in the platform's permission model but no endpoint uses them today. This page reflects three shipped endpoints, not a roadmap — ask Angie Eats for a timeline rather than assuming any of this is coming imminently.
orders:writePlacing an order on a customer's behalf (a till, an aggregator).
orders:readReading order status (a KDS, a reporting tool).
orders:advanceAdvancing an order through the kitchen (accept, ready, complete) as a partner system.
delivery:readDelivery zone and fee lookups for a partner's own checkout.
webhooks:manageWebhooks — a push notification when an order or menu changes. There is no subscription system behind this scope yet; poll the
GETendpoints above if you need to know about changes today.