Skip to main content
Every failure — validation, business rule, rate limit, or an unexpected fault — comes back in the same shape. There is no second format to special-case.

Error response format

string
Machine-readable. Branch on this, not on message — the wording may change, the code will not.
string
Human-readable, and localised where a translation exists — by the locale saved on your account, falling back to Accept-Language. Safe to show a user, not safe to parse.
object | array
Extra context for the codes that carry it — required on INSUFFICIENT_FUNDS, retry_after on RATE_LIMIT_EXCEEDED. An empty object when there is nothing to add, so the key is always present.
Correlation id for this request, also returned as the X-Request-ID header. Quote it when contacting support. You may send your own X-Request-ID on the way in and it will be echoed back.

Error codes

A missing resource and one owned by another account both answer 404. That is deliberate: a 403 here would confirm the id exists.
When a 403 comes from a feature being switched off rather than from permissions, details.feature names the flag — the same name GET /flags reports, so you can check availability before calling.

Validation errors

VALIDATION_ERROR is the one code where details is an array rather than an object — one entry per field that failed:
field is the dotted path to the offending value, starting with the part of the request it came from (body, query, path, header).

Rate limits

Two limits apply, and either can produce a 429. A global limit of 100 requests per 60 seconds, with a burst allowance of 10. It is counted per caller, identified in this order: your X-API-Key, then your authenticated user, then your IP address. An API key and a bearer token for the same account are therefore two separate budgets. Per-route limits on the expensive endpoints, which are much tighter. For example POST /ip-check allows 5 requests per 60 seconds with a burst of 2 — each call holds a connection open for the length of three live third-party lookups.

Reading a 429

The delay is reported in both places a client might look:
Retry-After is always a whole number of seconds — never an HTTP-date — and details.retry_after is the same number as a JSON integer. Use either.
A few limits have no meaningful wait to quote. Those answer 429 with an empty details and no Retry-After; back off on your own schedule rather than reading a missing header as zero.
X-RateLimit-Reset is a Unix timestamp in seconds. The X-RateLimit-* headers describe the global limit and accompany successful responses too, so you can slow down before being refused.

Handling errors

The Python SDK maps each status onto its own exception, so the common cases are except clauses rather than branches on a code:
409 covers both a genuine conflict and an empty wallet, so error.code is what separates them. Every exception carries status_code, code, request_id and the parsed response_body — read details from the last of those when you need it, for example exc.response_body["error"]["details"]["required"]. Retry 429, 500 and 503. Do not retry 4xx codes other than 429 — the request will fail the same way until you change it. When retrying anything that spends money, send an Idempotency-Key so a retry cannot charge twice; see Idempotency.