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…"
}| Status | Code | Meaning | Retry? |
|---|---|---|---|
| 400 | invalid_request | Malformed JSON or an unknown field | No — fix the call |
| 401 | unauthenticated | Missing, expired or revoked credential | No — re-auth |
| 403 | insufficient_scope | Authenticated, but the scope is not granted | No |
| 404 | not_found | No such row in *this* tenant | No |
| 409 | idempotency_conflict | Key reused with a different body | No |
| 422 | validation_failed | Shape is fine, values are not | No |
| 429 | rate_limited | Bucket exhausted | Yes, after `retry-after` |
| 503 | upstream_unavailable | A dependency is down or timed out | Yes, 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);
}