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

Conventions

The response envelope, pagination, dates, CORS and caching. Rules that hold across every endpoint.

A full brief for whoever builds against this.

Read this once and every module page gets shorter, because none of them repeat what is here.

The response envelope#

Keyed endpoints answer with the same wrapper, success or failure. The payload is always under data, never at the top level.

Success
{
  "success": true,
  "data": { "id": "8f2a...", "title": "Beef tartare" }
}
Failure
{
  "success": false,
  "error": {
    "message": "Validation failed",
    "code": "VALIDATION_ERROR",
    "details": [
      { "path": ["name"], "message": "Name is required", "code": "too_small" }
    ]
  }
}

Check the HTTP status first and success second. They agree, but the status is what your fetch client, your logs and your monitoring already understand. Branch on code; the message is for your logs.

details is there only when a failure has more to say. A body that failed validation lists its fields there, one entry per problem, each with a path, a message and a code and nothing else, and the message itself is always Validation failed. The ordering endpoints are the exception: the quote, the checkout and the promotion preview name the failing path in the message itself — fulfillment.address: unknown field(s) housenumber — and send no details. See Errors.

Public endpoints are plainer

A few storefront endpoints answer with a bare array or object instead of the envelope, because they are read straight into a browser. /api/public/{org}/fees returns an array; /api/public/{org}/payment-methods returns { stripe, custom }. Each page says which shape to expect.

Pagination#

List endpoints take page and limit and return a pagination block next to the data.

json
{
  "success": true,
  "data": [ ],
  "pagination": {
    "page": 1,
    "limit": 50,
    "total": 128,
    "totalPages": 3,
    "hasMore": true
  }
}
ParameterDefaultNotes
page1One-based.
limit10Ask for what you need. 50 is a sensible page size for a menu or a listing.

The default of 10 catches people out. A menu of 40 dishes renders 10 items and looks like a data problem, so ask for what you need. And when hasMore is true, fetch the next page rather than assuming one page is everything.

Dates and numbers#

KindFormatExample
TimestampISO 8601, UTC2026-07-26T18:30:00.000Z
Calendar dateYYYY-MM-DD, venue timezone2026-12-25
Wall clock timeHH:mm, 24 hour, venue timezone17:30
MoneyDecimal number, not cents24.20
PercentageNumber, not a fraction9 means nine percent
IdentifierUUID v4 string8f2a1c34-...

Seconds must be 00 where a value has to line up with a bookable slot. A reservation at 18:00:30 is rejected rather than rounded, because rounding silently moves someone's table.

Unknown fields#

On the ordering endpoints — the quote and the checkout — the nested objects, fulfillment and its address and the options on a line, reject keys they do not recognise, and the error names the offending key.

text
400  fulfillment.address: unknown field(s) housenumber

That is deliberate. A silently dropped address is a courier standing in the wrong street two hours later. A 400 during development is thirty seconds.

The top level of a request body is more forgiving, so older storefronts keep working: anything unrecognised there is thrown away. The checkout, the order create (POST /api/orders), the reservation create, the booking create and the contact form submit list it back in data.ignored_fields on success, and only when there is something to list — if a field you sent shows up in that list, it did not arrive, whatever the 2xx suggests. Other endpoints drop it without a word, so a misspelled key there is only visible in what was stored.

CORS#

Public endpoints send Access-Control-Allow-Origin: * and answer preflight requests, so they work from any origin without configuration.

Keyed endpoints do not, and that is not a gap to work around. If a browser request to a keyed endpoint fails on CORS, the actual problem is that a secret is about to be shipped to a browser. Move the call to your server.

Caching#

Cache content reads, briefly. Your customer edits a page expecting to see the change, and a day-long cache makes their dashboard look broken. A minute or two, or on-demand revalidation, is the right order of magnitude.

ts
// Next.js
fetch(url, { headers, next: { revalidate: 60 } })

Never cache anything priced. Payment methods, fees, promotions and totals are read at the moment of purchase for a reason. The same goes for availability: a slot that has just filled must stop being offered, so the endpoint answers no-store — and a revalidate of your own would undo that.

Failing softly#

A failed content fetch is a reason to render the page without that section, not to fail the page. A site that goes blank because a news block could not load is worse than a site with no news block.

The exception is anything to do with money. If payment methods come back empty, hide the checkout button rather than letting someone fill in a form that cannot submit.

PreviousAuthenticationNextErrors

On this page

  • The response envelope
  • Pagination
  • Dates and numbers
  • Unknown fields
  • CORS
  • Caching
  • Failing softly