CnterDocumentationSign in

Start here

  • What Cnter is
  • Core concepts
  • Quickstart

Platform

  • Authentication
  • Conventions
  • Errors
  • Organization
  • The dashboard

Modules

  • Content Management
  • Online Ordering
  • Restaurant Ordering
  • Promotions
  • Reservations
  • Bookings
  • Contact Form

Payments

  • AgnusPAY
  • Tax

Services

  • Address autocomplete
  • Video
  • Email

Reference

  • API reference

Platform

Authentication

API keys, scopes, headers and rate limits. Plus the endpoints that need none of it.

A full brief for whoever builds against this.

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.

Shown once, stored hashed

The key is stored as a SHA-256 hash. Nobody can read it back afterwards. If it is lost, the only path forward is creating a new one and swapping it in.

Send it in a header on every request:

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

ScopeGrants
read_contentRead content types, items and the generated content docs. The scope almost every integration needs.
read_productsGrants 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_ordersList and read orders, including their line items.
write_ordersCreate and update orders from outside the dashboard.
create_ordersStorefront 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_bookingsList and read bookings.
write_bookingsCreate bookings, which also sends the confirmation email.
read_reservationsList and read reservations.
write_reservationsCreate, update and cancel reservations.
read_reservation_settingsRead opening hours, slot configuration and date overrides. What a booking widget needs.
write_reservation_settingsChange those settings. A storefront should never hold this.
contact_form_submitSubmit a contact form entry. Nothing else. The only scope a public website form needs.
contact_form_manageRead, update, delete and reply to submissions. Setting the module up (switching it on, its templates and notifications) is dashboard-only.

One key, several scopes

If you were handed briefs for more than one module, they name the same base URL and the same organization. Use one key carrying every scope those modules need rather than juggling several.

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}/restaurant and the other storefront reads
  • //api/public/{organizationId}/payment-methods and /api/public/{organizationId}/fees
  • //api/public/{organizationId}/quote, which prices a whole basket
  • //api/public/{organizationId}/restaurant/delivery-check
  • //api/public/{organizationId}/address/suggest and /address/lookup
  • //api/public/{organizationId}/promotions/preview
  • //api/public/video/{videoId}/thumbnail
  • //api/online-ordering/checkout when paying online, where the organization id is a query parameter. Paying in person on the same address needs a key with create_orders; the client calls that path checkoutInPerson

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#

StatusCodeMeaning
401API_KEY_MISSINGNo X-API-Key header and no api_key parameter.
401INVALID_API_KEYNo active key matches. Usually a whitespace or copy error.
401API_KEY_EXPIREDThe key passed its expiry date.
403PERMISSION_DENIEDValid key, missing scope. The message names it: Missing required permission: read_content.
403DASHBOARD_ONLYThe endpoint is dashboard work and refuses keys, whatever scope they carry.
429RATE_LIMIT_EXCEEDEDOver the key's limit for the current window.

The full list of statuses, including the ones specific to checkout, is in Errors.

PreviousQuickstartNextConventions

On this page

  • API keys
  • Keys are server-side
  • Scopes
  • Public endpoints
  • Rate limits and expiry
  • When authentication fails