You are reading v0, which is retired on 2026-06-30. It is kept online for existing integrations only. Read the v1 version.

Errors

Problem responses, the stable error codes, which ones are safe to retry and how to log them without leaking data.

Version v0 · updated 2026-02-01

Errors are RFC 9457 problem documents. The `code` is the stable contract — branch on it. The `detail` string is written for a human reading a log and may change without notice.

{
  "type": "https://framique.com/docs/v1/errors#insufficient_scope",
  "title": "Insufficient scope",
  "status": 403,
  "code": "insufficient_scope",
  "detail": "This credential does not carry orders.write.",
  "request_id": "req_01JB4…"
}
StatusCodeMeaningRetry?
400invalid_requestMalformed JSON or an unknown fieldNo — fix the call
401unauthenticatedMissing, expired or revoked credentialNo — re-auth
403insufficient_scopeAuthenticated, but the scope is not grantedNo
404not_foundNo such row in *this* tenantNo
409idempotency_conflictKey reused with a different bodyNo
422validation_failedShape is fine, values are notNo
429rate_limitedBucket exhaustedYes, after `retry-after`
503upstream_unavailableA dependency is down or timed outYes, with backoff

Retry policy we expect#

const RETRYABLE = new Set([429, 502, 503, 504]);

export async function call(url: string, init: RequestInit, attempt = 0): Promise<Response> {
  const res = await fetch(url, init);
  if (!RETRYABLE.has(res.status) || attempt >= 5) return res;

  const after = Number(res.headers.get("retry-after"));
  // Full jitter: a fleet retrying in lockstep is a self-inflicted outage.
  const backoff = Number.isFinite(after) && after > 0
    ? after * 1000
    : Math.random() * Math.min(30_000, 2 ** attempt * 500);

  await new Promise((r) => setTimeout(r, backoff));
  return call(url, init, attempt + 1);
}