Skip to content
Angie EatsSet your addressYour basket is empty
← Back

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:

Header
Authorization: Bearer zpk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

or, if a custom header is easier for your HTTP client than Authorization:

Header
X-Api-Key: zpk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Both 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.

ScopeGrantsUsed today
menu:readRead restaurants, opening hours and menus.Yes
menu:writeChange prices, descriptions and whether a dish is sold out.Yes
orders:writePlace orders.Not yet — no endpoint
orders:readRead orders and their current status.Not yet — no endpoint
orders:advanceMove an order through the kitchen — accept, ready, completed.Not yet — no endpoint
delivery:readRead delivery areas, fees and whether an address is covered.Not yet — no endpoint
webhooks:manageCreate 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.

GET/partner/restaurantsmenu:read

Lists 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.

Request
curl -s https://eats.heyangie.ai/api/v1/partner/restaurants \
  -H "Authorization: Bearer $ANGIE_EATS_KEY"
Real response
{
  "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.

GET/partner/restaurants/{restaurant}/menumenu:read

The full menu: sections, items, sizes, and modifier groups with their modifiers. Every price is an integer minor unit — see Money below.

Request
curl -s https://eats.heyangie.ai/api/v1/partner/restaurants/25/menu \
  -H "Authorization: Bearer $ANGIE_EATS_KEY"
Real response
{
  "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.

POST/partner/restaurants/{restaurant}/availabilitymenu:write

Marks 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.

Request
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}]}'
Real response
{ "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.

StatusMeaningRetry?
401No key, a wrong key, a revoked key, or an expired key. The last three return the identical message on purpose.No
403Missing scope ("This key does not have the … permission.") or a suspended partner account — two different messages, one status.No
404The restaurant doesn't exist, or exists but is outside your grant. Both look identical.No
422Request body failed validation.No
429This 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:

Real responses
$ 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.

Real response, from a key deliberately issued at 3/minute
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:

Formula
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, with v1 kept running for a publicized deprecation window. There is no v2 today.

Your first integration in ten minutes

  1. 1. Get a key.

    Email partners@eats.heyangie.ai with your market and menu:read (and menu:writeif you'll report sold-out items). Save the zpk_live_…string somewhere real before you close that message — it won't be shown again.

  2. 2. List what you can see.

    Terminal
    curl -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 empty restaurants array 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. 3. Pull one restaurant's menu.

    Terminal
    curl -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. 4. If you have menu:write, mark one item sold out and confirm it sticks.

    Terminal
    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":<item id from step 3>,"is_sold_out":true}]}'

    Check updated matches what you sent and ignored is empty, then re-fetch the menu to confirm is_sold_out: true. Flip it back when you're done testing.

  5. 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-empty ignoredon 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:write

    Placing an order on a customer's behalf (a till, an aggregator).

  • orders:read

    Reading order status (a KDS, a reporting tool).

  • orders:advance

    Advancing an order through the kitchen (accept, ready, complete) as a partner system.

  • delivery:read

    Delivery zone and fee lookups for a partner's own checkout.

  • webhooks:manage

    Webhooks — a push notification when an order or menu changes. There is no subscription system behind this scope yet; poll the GET endpoints above if you need to know about changes today.