Platform
Conventions
The response envelope, pagination, dates, CORS and caching. Rules that hold across every endpoint.
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": true,
"data": { "id": "8f2a...", "title": "Beef tartare" }
}{
"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.
Pagination
List endpoints take page and limit and return a pagination block next to the data.
{
"success": true,
"data": [ ],
"pagination": {
"page": 1,
"limit": 50,
"total": 128,
"totalPages": 3,
"hasMore": true
}
}| Parameter | Default | Notes |
|---|---|---|
page | 1 | One-based. |
limit | 10 | Ask 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
| Kind | Format | Example |
|---|---|---|
| Timestamp | ISO 8601, UTC | 2026-07-26T18:30:00.000Z |
| Calendar date | YYYY-MM-DD, venue timezone | 2026-12-25 |
| Wall clock time | HH:mm, 24 hour, venue timezone | 17:30 |
| Money | Decimal number, not cents | 24.20 |
| Percentage | Number, not a fraction | 9 means nine percent |
| Identifier | UUID v4 string | 8f2a1c34-... |
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.
400 fulfillment.address: unknown field(s) housenumberThat 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.
// 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.