Errors + rate limits
One error envelope across every endpoint. Standard HTTP status codes
+ a structured code string for programmatic matching.
Rate limit headers on every response so you can pace yourself before
you hit a 429.
Error envelope
{
"detail": "Human-readable summary.",
"code": "billing.subscription_past_due",
"field_errors": {
"scheduled_for": ["Must be a future ISO 8601 timestamp."]
}
} detail— always present, safe to render to humans (it's translated where the locale is available).code— present for distinguishable error states. Stable across API versions; match on this for retry logic.field_errors— present on 400 validation failures only. Map of field-name → list of messages.
Status code matrix
| Status | Class | Retry? | Common causes |
|---|---|---|---|
| 400 | Validation | No | Missing required field, bad format, business-rule violation |
| 401 | Auth | No | Missing / bad / revoked credential |
| 402 | Billing | No | Subscription past due, free-plan limit hit |
| 403 | Permission | No | Credential lacks scope, or resource is in a different org |
| 404 | Not found | No | Resource doesn't exist, or isn't visible to your credential |
| 409 | Conflict | Sometimes | Idempotency-Key reuse mismatch, state machine violation |
| 422 | Unprocessable | No | Semantically invalid combination of valid fields |
| 429 | Rate limited | Yes, with backoff | Per-credential or per-org throttle hit |
| 500 | Server error | Yes, with backoff | Unexpected — open a ticket if persistent |
| 502 / 503 / 504 | Transient | Yes, with backoff | Upstream / deploy / load |
Notable error codes
| Code | Status | What it means |
|---|---|---|
billing.subscription_past_due | 402 | Org's Stripe subscription is past due. Direct admin to /admin/billing. |
billing.free_plan_minutes_exhausted | 402 | Free / Pilot tier has used its 5 inspections. |
billing.signature_pack_exhausted | 402 | Signature pack credits used up; pay-per-sig or buy another pack. |
permission_denied | 403 | Credential lacks the scope. See field_errors.required_scope. |
idempotency.key_mismatch | 409 | Same key, different body. Use a fresh key. |
session.already_ended | 409 | Calling end on a non-open session (usually safe — end is idempotent, this is for the edge cases). |
kyb.not_verified | 403 | QES signature requested on an org that hasn't cleared KYB. |
rate_limited | 429 | Per-credential (60 rpm) or per-org (600 rpm) throttle hit. |
Rate limit headers
Every response — success or error — carries the throttle state:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1716461235 X-RateLimit-Limit— the cap that applies to this request.X-RateLimit-Remaining— how many calls remain in the current window.X-RateLimit-Reset— Unix timestamp at whichRemainingresets.
On a 429, Retry-After is also set (in seconds) per RFC 6585.
Throttle scopes
| Scope | Limit | Window |
|---|---|---|
| Per credential | 60 | 1 minute |
| Per org (across all credentials) | 600 | 1 minute |
Burstable for the first second of a minute. Hit the cap and further requests within the window 429.
Retry strategy
Exponential backoff with jitter for 429 / 5xx; do not retry 4xx (other than 408, 425, 429):
async function withRetry<T>(fn: () => Promise<T>, max = 5): Promise<T> {
let lastErr: unknown;
for (let i = 0; i < max; i++) {
try {
return await fn();
} catch (err: any) {
const status = err?.status;
if (status >= 400 && status < 500 && ![408, 425, 429].includes(status)) {
throw err; // not retryable
}
const base = status === 429 ? (err.retryAfterMs ?? 1000) : 250 * 2 ** i;
const jitter = Math.random() * base * 0.2;
await new Promise((r) => setTimeout(r, base + jitter));
lastErr = err;
}
}
throw lastErr;
}
The SDKs implement this internally on 429 + 5xx; if you'd rather
handle retries yourself, set retries: 0 in the SDK
options.
Error correlation
Every response carries a X-Request-Id header (UUID). Quote
this in any support ticket — we can pull the full request trace from
that id alone.
What's next
- Authentication — credential rotation if you're seeing 401s
- Pagination + idempotency — the patterns that interact with retries