Errors
Problem responses, the stable error codes, which ones are safe to retry and how to log them without leaking data.
Version v1 · 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);
}