LIVE · AUDIT-CHAINED · EU-RESIDENT
SYSTEM · 99.99% UPTIME
v 1.0 ↗ MADE IN EU

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

StatusClassRetry?Common causes
400ValidationNoMissing required field, bad format, business-rule violation
401AuthNoMissing / bad / revoked credential
402BillingNoSubscription past due, free-plan limit hit
403PermissionNoCredential lacks scope, or resource is in a different org
404Not foundNoResource doesn't exist, or isn't visible to your credential
409ConflictSometimesIdempotency-Key reuse mismatch, state machine violation
422UnprocessableNoSemantically invalid combination of valid fields
429Rate limitedYes, with backoffPer-credential or per-org throttle hit
500Server errorYes, with backoffUnexpected — open a ticket if persistent
502 / 503 / 504TransientYes, with backoffUpstream / deploy / load

Notable error codes

CodeStatusWhat it means
billing.subscription_past_due402Org's Stripe subscription is past due. Direct admin to /admin/billing.
billing.free_plan_minutes_exhausted402Free / Pilot tier has used its 5 inspections.
billing.signature_pack_exhausted402Signature pack credits used up; pay-per-sig or buy another pack.
permission_denied403Credential lacks the scope. See field_errors.required_scope.
idempotency.key_mismatch409Same key, different body. Use a fresh key.
session.already_ended409Calling end on a non-open session (usually safe — end is idempotent, this is for the edge cases).
kyb.not_verified403QES signature requested on an org that hasn't cleared KYB.
rate_limited429Per-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 which Remaining resets.

On a 429, Retry-After is also set (in seconds) per RFC 6585.

Throttle scopes

ScopeLimitWindow
Per credential601 minute
Per org (across all credentials)6001 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