Errors and rate limits¶
Two response shapes¶
Clix returns errors in one of two shapes depending on which layer refused the call. Handle both.
Middleware — throttling and the fair-use guard — uses detail. Almost everything else uses message. Validation failures put an array in message.
Validation errors are 400, not 422
Clix replaces the framework default. A malformed body returns 400 with a list:
Status codes¶
| Code | Meaning | Retry? |
|---|---|---|
400 |
Validation failed, or a business rule refused it | No — fix the request |
401 |
Missing, invalid or revoked key | No |
403 |
Permission, inactive org, plan without API access, or fair use | No |
404 |
Not found, or not yours | No |
409 |
Organisation context required | No — send X-Active-Org |
429 |
Rate limited | Yes, after Retry-After |
5xx |
Server side | Yes, with backoff |
The 401s and 403s worth distinguishing¶
| Body | Cause |
|---|---|
{"status":"false","message":"Invalid or revoked API key"} |
Unknown, revoked, or a deactivated organisation |
{"status":"false","message":"API access is not included in your current plan."} |
Plan without API access — checked on every request |
{"message":"Your account does not have permission to perform this action."} |
The key's role or scopes do not cover this endpoint |
{"message":"Your organization's account is inactive. Please contact support."} |
Organisation suspended |
Machine-readable codes¶
Most errors carry prose only. These are the stable code values:
code |
Status | Meaning |
|---|---|---|
fair_use_limit |
403 | The organisation hit its fair-use ceiling for the month |
org_context_required |
409 | The key resolves to more than one organisation |
invalid_request |
400 | Malformed key-management request |
not_found |
404 | No such key |
invalid_role, invalid_scopes |
400 | Bad role or scope at key creation |
key_id_unavailable |
503 | Could not allocate a key id — retry |
Branch on code where one exists. Never parse the prose.
Rate limits¶
Fixed one-minute window, enforced per key.
| Limit | Default |
|---|---|
| Per authenticated caller | 200 requests / minute |
| Per IP address | 100 requests / minute |
| Window | 60 seconds |
Each API key gets its own bucket — two keys in one organisation do not share the 200.
Every response carries the state:
X-RateLimit-Reset is seconds until the window resets, not a timestamp. On a 429 it equals Retry-After.
A 429 adds Retry-After and returns:
{ "detail": "Rate limit exceeded. Please try again later.",
"limit": 200, "window": 60, "retry_after": 12 }
Respect Retry-After. Read X-RateLimit-Remaining and slow down before you hit zero rather than after.
Fair use¶
API calls are not metered against a monthly API quota. What can refuse a call is a fair-use guard on write operations — creating, updating and deleting — when an organisation is far outside normal patterns:
{ "code": "fair_use_limit",
"detail": "Your organisation has reached its fair-use limit for this month. Viewing and downloading still work. Contact support to continue.",
"current_usage": 1234, "limit": 1000 }
It is a 403, not a 429, and retrying will not clear it. Reads keep working. Talk to support.
Not counted: reads, previews, PDF downloads, authentication and subscription calls.
api_calls_used is always 0
GET /api/usage/current/ returns api_calls_used: 0 and api_calls_limit: 0 permanently, kept so older clients keep parsing. The meaningful figures there are the invoice counters. See Usage and quotas.
What to retry¶
Retry 429 after Retry-After, and 5xx with exponential backoff.
Do not retry 400, 401, 403, 404, 409.
Never blind-retry POST /api/invoices/. Identical payloads are refused for five minutes with a 400, and a timeout does not mean the invoice was not created. Poll first. A cleared invoice cannot be deleted — only credited.
flowchart TD
A[Response] --> B{Status}
B -->|429| C[Wait Retry-After, then retry]
B -->|5xx| D[Retry with exponential backoff]
B -->|400 401 403 404 409| E[Do not retry: fix the request, key, plan or id]
B -->|timeout on POST /api/invoices/| F[Poll the invoice list first]
F -->|exists| G[Use it]
F -->|absent| H[Retry once]