Platform
Authentication
API keys, scopes, headers and rate limits. Plus the endpoints that need none of it.
There are three ways to reach the API, and which one applies is a property of the endpoint, not a choice you make. Public endpoints take no credentials. Integration endpoints take an API key. The dashboard's own endpoints take a signed-in session and are not part of the public surface.
API keys
A key belongs to one organization and carries a fixed set of scopes. Your customer creates it under Settings then Organization then API Keys, choosing the scopes and, optionally, an expiry date.
Send it in a header on every request:
X-API-Key: <your key>
x-organization-id: <organization uuid>The organization header is belt and braces. The key already determines the tenant, so a mismatched id cannot widen access, but sending it keeps logs and error messages legible when an agency runs several organizations from one codebase.
A key may also be passed as an api_key query parameter. It exists for callers that cannot set headers, such as a webhook field in third-party software. Prefer the header: query strings end up in access logs, browser history and referrer headers.
Keys are server-side
The key must not reach the browser. Not in a NEXT_PUBLIC_ variable, not in a client component, not in a fetch that runs on the page. Anyone reading your page source could then read this customer's content and, with the wrong scopes, write their orders.
Fetch on your server and render the result. When the browser genuinely needs to make the call, such as address autocomplete typed into a form, use the public endpoints, which are built for exactly that and expose nothing private.
Scopes
Ask for the narrowest set that does the job. A storefront that reads a menu and takes pay-in-person orders needs two scopes; giving it the other eleven turns a leaked key from an inconvenience into an incident.
| Scope | Grants |
|---|---|
read_content | Read content types, items and the generated content docs. The scope almost every integration needs. |
read_products | Grants nothing. The product catalog it covered moved into Content Management, and /api/v1/products now answers 410 for everyone. Ask for read_content instead. |
read_orders | List and read orders, including their line items. |
write_orders | Create and update orders from outside the dashboard. |
create_orders | Storefront checkout only: create a pending order through /api/online-ordering/checkout — checkoutInPerson in the client, which sends the key there and nowhere else in ordering. Cannot read or change an existing order. |
read_bookings | List and read bookings. |
write_bookings | Create bookings, which also sends the confirmation email. |
read_reservations | List and read reservations. |
write_reservations | Create, update and cancel reservations. |
read_reservation_settings | Read opening hours, slot configuration and date overrides. What a booking widget needs. |
write_reservation_settings | Change those settings. A storefront should never hold this. |
contact_form_submit | Submit a contact form entry. Nothing else. The only scope a public website form needs. |
contact_form_manage | Read, update, delete and reply to submissions. Setting the module up (switching it on, its templates and notifications) is dashboard-only. |
Public endpoints
A handful of endpoints take no credentials at all. They carry the organization id in the path, return only what a storefront visitor is entitled to see, and send permissive CORS headers so they can be called straight from a browser.
/api/public/{organizationId}/restaurantand the other storefront reads/api/public/{organizationId}/payment-methodsand/api/public/{organizationId}/fees/api/public/{organizationId}/quote, which prices a whole basket/api/public/{organizationId}/restaurant/delivery-check/api/public/{organizationId}/address/suggestand/address/lookup/api/public/{organizationId}/promotions/preview/api/public/video/{videoId}/thumbnail/api/online-ordering/checkoutwhen paying online, where the organization id is a query parameter. Paying in person on the same address needs a key withcreate_orders; the client calls that pathcheckoutInPerson
Every one of these except the video thumbnail also answers on a versioned contract address — the quote, for instance, at /api/client/v1/{organizationId}/quote. That is the one a storefront should call, and the one the client script names for you so you never write it down. The addresses listed above are the originals: they keep answering and promise nothing about their shape.
Being public is a design decision, not an oversight. These endpoints are deliberately narrow, and the same rule shapes two of them: neither the promotions preview nor the quote accepts an email address, because answering differently for someone who has ordered before would turn either into a way to test whether a given person is a customer. It is why a promotion limited to a first order is applied by the checkout and not visible in a quote.
Rate limits and expiry
Each key has its own limit, set when it is created and adjustable afterwards. The default is 1000 requests per hour. Exceeding it returns 429 RATE_LIMIT_EXCEEDED; back off and retry rather than looping.
A key may carry an expiry date. Past it, every request returns 401 API_KEY_EXPIRED. If you set one, calendar the swap: an expiry nobody remembers is an outage on a date nobody chose.
Keys can also be deactivated in the dashboard, which takes effect within about fifteen seconds. That is the response to a leak: deactivate, create a new key, deploy.
Last use is recorded per key, so your customer can see which of their integrations is still live before revoking one.
When authentication fails
| Status | Code | Meaning |
|---|---|---|
401 | API_KEY_MISSING | No X-API-Key header and no api_key parameter. |
401 | INVALID_API_KEY | No active key matches. Usually a whitespace or copy error. |
401 | API_KEY_EXPIRED | The key passed its expiry date. |
403 | PERMISSION_DENIED | Valid key, missing scope. The message names it: Missing required permission: read_content. |
403 | DASHBOARD_ONLY | The endpoint is dashboard work and refuses keys, whatever scope they carry. |
429 | RATE_LIMIT_EXCEEDED | Over the key's limit for the current window. |
The full list of statuses, including the ones specific to checkout, is in Errors.