Requests & responses
The same envelope, the same filters and the same errors on every endpoint. What you learn on one resource carries to all of them.
Response envelope
A single record:
{
"data": { "id": "01hz…", "name": "Latte" }
}
A list, with pagination in meta:
{
"data": [ … ],
"meta": { "total": 42, "page": 1, "per_page": 25, "last_page": 2 }
}
An error — always these three keys, never anything else:
{
"error": {
"code": "insufficient_scope",
"message": "This endpoint requires the scope \"products.write\", …",
"details": { "missing_scopes": ["products.write"] }
}
}
code is for machines and never changes. message is for
people and may improve. Branch on the code.
Pagination
| Parameter | Description |
|---|---|
| page | Page number, starting at 1. |
| per_page | Page size, default 25, maximum 100. |
A per_page above the maximum is rejected, not silently
clamped: quietly returning fewer rows than asked for makes an integration
think it has synced everything when it has not.
Incremental sync
GET /products?updated_since=2026-01-01T00:00:00Z
Every list accepts updated_since. Store the timestamp of your last
successful sync and pass it back to fetch only what changed since.
Filtering and sorting
| Parameter | Description |
|---|---|
| active | true or false. Omit it to get both. |
| sort | Column to sort by. Each endpoint lists what it accepts; an unknown column is rejected rather than ignored. |
| direction | asc or desc. |
Partial updates
PATCH changes only the fields you send; everything else is left
alone. You never have to read a record, modify one field and write the whole
thing back.
Lists of related ids (branch_ids, product_ids,
addon_group_ids) are the exception: when present they
replace the whole set. Omit them to leave the links untouched.
Language
GET /products?lang=en
Translatable fields (product name and description, category and addon names) come back in the requested language, falling back to the brand's default where a translation is missing. Only languages configured in the panel are accepted — an unknown code is an error, not a silent fallback.
Write endpoints take a translations object keyed by language code.
Money and dates
Amounts are JSON numbers in the brand's currency, rounded to two decimals.
All timestamps are ISO-8601 with an offset; date-only fields are
YYYY-MM-DD. Identifiers are ULIDs — sortable, 26 characters.
Rate limits
120 requests per minute per key, so one
integration cannot eat another's quota. Unauthenticated requests are limited
far more tightly per IP. Exceeding the limit returns
rate_limited with a Retry-After header.