Praxsuite

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

INVALID_REQUEST

Neither query nor mutation, or both at once

400

INVALID_REFS

refs was empty, held an unknown GUID, held a GUID from another workspace, or exceeded 20 entries

400

INVALID_QUERY / INVALID_MUTATION

The body parsed but does not make sense against the schema

401

UNAUTHORIZED

No credential, or one that is not valid for this workspace

403

FORBIDDEN

The credential belongs to a different workspace

403

SCOPE_VIOLATION

A table in the request is outside the credential's scope, or aggregations are not allowed on it

403

NOTIFY_DENIED

The mutation asked to announce on the Event Bus and the credential may not

408

QUERY_TIMEOUT / MUTATION_TIMEOUT

The operation ran past the execution timeout

429

RATE_LIMIT_EXCEEDED

Too many calls this minute

429

QUOTA_EXCEEDED

The monthly API-call allowance is spent

429

EGRESS_LIMIT_EXCEEDED

The monthly included response bytes are spent

500

EXECUTION_ERROR

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: 100000

Separately, 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 limit when you omit it

50

Tables in refs

20

Relation nesting depth

5

Columns in select

100

Conditions in where

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

X-Cache

HIT or MISS

X-Cache-Credential

Which key's cache answered — set on a HIT only

ETag

A validator you can send back

Cache-Control

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

Cache-Control

Time-based

private, max-age=<ttl>

Write-invalidated

private, no-cache, must-revalidate

Hybrid

private, max-age=<ttl>, must-revalidate

Off

no-store

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 /query

  • Schema — GET /schema