Skip to content

Errors and rate limits

Two response shapes

Clix returns errors in one of two shapes depending on which layer refused the call. Handle both.

{ "message": "…" }
{ "detail": "…" }

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:

{ "message": ["buyer.vat_number: field required"] }

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-Limit: 200
X-RateLimit-Remaining: 187
X-RateLimit-Reset: 60

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]