Errors and limits
Vincent Depassier · September 17, 2026
The three error shapes
POST /query returns a structured envelope:
{
"error": {
"code": "SCOPE_VIOLATION",
"message": "No access to table 'invoices'.",
"details": ["..."]
}
}details is present only when there is more than one thing to say — a list of validation failures, typically. Write your client against code; the message is for a human reading a log and may be reworded.
Most of /auth/* — register, login, refresh, the password routes, the provider routes — returns the service envelope instead, on success and on failure alike:
{
"isSuccess": false,
"statusCode": 400,
"message": "Password must be at least 8 characters.",
"data": null,
"errors": ["Password must be at least 8 characters."]
}Everything else — the authentication layer, files, schema, endpoints — returns a flat string:
{ "error": "Origin not allowed for this credential." }There is no code in either of the last two. Branch on the HTTP status.
The players routes are a small variant of the flat shape, carrying a code beside the message: { "code": "PLAYER_NOT_FOUND", "message": "…" }. The MCP endpoint is different again — it answers in JSON-RPC.
Codes returned by /query
HTTP | Code | What happened |
400 |
| Neither |
400 |
|
|
400 |
| The body parsed but does not make sense against the schema |
401 |
| No credential, or one that is not valid for this workspace |
403 |
| The credential belongs to a different workspace |
403 |
| A table in the request is outside the credential's scope, or aggregations are not allowed on it |
403 |
| The mutation asked to announce on the Event Bus and the credential may not |
408 |
| The operation ran past the execution timeout |
429 |
| Too many calls this minute |
429 |
| The monthly API-call allowance is spent |
429 |
| The monthly included response bytes are spent |
500 |
| Internal failure; the details are recorded server-side |
The three 429s are worth distinguishing in your client. A rate limit clears by itself within the minute and deserves a retry with backoff. A quota or egress limit does not clear until the billing period rolls over or someone changes the plan — retrying is pointless, and the right behaviour is to surface it.
Limits
Two allowances are metered per workspace and per month: API calls and egress, the bytes your responses carry. Both are visible in API Gateway → Usage, and both can be lifted by the plan or by enabling pay-as-you-go in the gateway settings.
When a workspace is over its included calls and pay-as-you-go is on, successful responses carry three extra headers so you can see it happening:
X-Api-Usage-Overage: true
X-Api-Usage-Current: 120431
X-Api-Usage-Included: 100000Separately, a per-minute rate limit protects the workspace from a runaway client. It is a property of the workspace, not of the individual key.
Structural limits on a request
These are not billing. They are fixed ceilings in the gateway, and exceeding one is a 400:
Limit | Value |
Rows returned by one query | 1000 (a table scope may set something lower) |
Default | 50 |
Tables in | 20 |
Relation nesting depth | 5 |
Columns in | 100 |
Conditions in | 50 |
Condition nesting depth | 5 |
Body of a custom endpoint call | 10 MB |
Page with limit and offset. Read meta.limit from the response rather than assuming it: the effective limit can be lower than the one you asked for.
Caching
A credential can be configured to cache POST /query responses — reads only; a body containing mutation is never cached. When caching is on, responses carry:
Header | Meaning |
|
|
| Which key's cache answered — set on a |
| A validator you can send back |
| The mode and TTL configured for that credential |
Send the ETag back as If-None-Match and a cached entry that still matches answers 304 Not Modified with no body, which is the cheapest read available and does not spend egress.
Cache-Control tells you which mode the credential is in:
Mode |
|
Time-based |
|
Write-invalidated |
|
Hybrid |
|
Off |
|
A cache is keyed per credential — two keys with different scopes never share an entry, which is what makes caching safe to turn on at all — and, in the invalidating modes, per table: a write to a table drops the cached responses that read from that table, and only those.
Next
Query —
POST /querySchema —
GET /schema