Errors
The error body shapes of the native, OpenAI and Anthropic routes, every status and code, and what to do about each one.
Errors are ordinary HTTP statuses with a JSON body. Branch on the status, then on the code when there is one, and show the human message to people but never parse it: messages are written for readers and may be reworded. Which body shape you get depends on the route family, so this page starts there.
Error body shapes
| Routes | Shape | Example |
|---|---|---|
Native /v1/*, /developer/*, /light/*, /engine/* | { "error": string, "code"?: string, ...details } | { "error": "This API key does not have the \"knowledge\" scope.", "code": "scope_denied" } |
OpenAI dialect: /v1/chat/completions, /v1/proofs/* | { "error": { "message", "type", "code", "param" } } | { "error": { "message": "Insufficient balance", "type": "insufficient_quota", "code": "insufficient_quota", "param": null } } |
Anthropic dialect: /v1/messages | { "type": "error", "error": { "type", "message" } } | { "type": "error", "error": { "type": "rate_limit_error", "message": "..." } } |
type ApiError = { status: number; code?: string; message: string; retryAfter?: number }
async function parseError(res: Response): Promise<ApiError> { const body: any = await res.json().catch(() => ({})) const err = body?.error const message = typeof err === 'string' ? err : err?.message ?? res.statusText const code = body?.code ?? (typeof err === 'object' ? err?.code ?? err?.type : undefined) const retry = Number(res.headers.get('retry-after')) return { status: res.status, code, message, retryAfter: retry > 0 ? retry : undefined }}Catalogue
400 and 413: the request is wrong
| Status | Code | Meaning | What to do |
|---|---|---|---|
| 400 | invalid_request_error (OpenAI and Anthropic) | messages missing or empty, or no user message. | Fix the payload. Retrying unchanged will fail again. |
| 400 | none | A native route rejected the body: a required field is missing, a value has the wrong type, scopes is not a list of known scopes, a limit is not a positive whole number, Collection not found, file is required (multipart/form-data). | Read error; it names the field. |
| 400 | text_required | POST /v1/verify was sent an empty text. | Send the text to check. |
| 400 | invalid_passages | passages on /v1/verify is malformed (each needs source and content). | See Le Vérificateur. |
| 400 | ambiguous_scope | /v1/verify received both passages and collectionIds. | Send one or the other. |
| 413 | text_too_large | The text sent to /v1/verify exceeds the maximum. | Split the text and verify the parts. |
| 413 | none | An uploaded file is over 25 MB. | Split or compress the file. |
401 and 403: who you are, what you may do
| Status | Code | Meaning | What to do |
|---|---|---|---|
| 401 | none | API key required..., Invalid or revoked API key, API key expired, or User not found. | Do not retry. Check the key, create a new one if needed. See Authentication. |
| 403 | scope_denied | The key does not carry the scope the route needs, or the route has no scope. | Add the scope. The message names it. |
| 403 | read_only_key | A read-only key tried to change something. | Use a key without readonly for writes. |
| 403 | client_suspended | The workspace is suspended by the integrator that manages it. | Contact that integrator. Keys keep working once access is restored. |
| 403 | provider_unpaid | The integrator behind this client workspace has an overdue invoice. | Contact your provider. |
402: a limit on what you have bought
A 402 never means "retry". It means a quota, a plan or a ceiling must change first. Nothing is consumed when a call is refused with a 402. See Rate limits and quotas for what each limit measures.
| Status | Code | Extra fields | Meaning | What to do |
|---|---|---|---|---|
| 402 | insufficient_quota | none | OpenAI dialect only: the workspace has no questions left for this call. | Add a question pack or move to a larger plan. |
| 402 | QUESTION_LIMIT | required, available | Native routes: fewer questions left than the request needs. required and available are counted in questions. | Add a pack or upgrade. |
| 402 | SUBSCRIPTION_INACTIVE | none | The trial is over or the subscription is no longer active. | Choose a plan in the console. |
| 402 | PLAN_UPGRADE_REQUIRED | feature, requiredPlan | The route belongs to a plan feature your plan does not include (for example the Registre needs the proof journal). | Upgrade to requiredPlan. |
| 402 | DOCUMENT_LIMIT | limit, used | The workspace reached its document volume. | Delete documents, add users, or upgrade. |
| 402 | SEAT_LIMIT | seats | The subscription covers fewer users than you tried to add. | Add a seat to the subscription. |
| 402 | API_KEY_BUDGET_EXCEEDED | none | This key reached its own monthly question ceiling, set below the workspace allowance. | Raise the key ceiling with PATCH /developer/api-keys/:id, or wait for the next month. |
404, 429, 5xx
| Status | Code | Meaning | What to do |
|---|---|---|---|
| 404 | none | The object does not exist in this workspace. Another workspace's object looks identical. | Check the id and the key's workspace. |
| 404 | model_not_found (OpenAI), not_found_error (Anthropic) | The agent:<slug> address matches no agent. | List addresses with GET /v1/models. |
| 404 | conversation_not_found | x-pulse-conversation names an unknown conversation. | Omit the header to start a new one. |
| 404 | proof_not_found | Unknown certificate id, or one from another workspace. | Use the id exactly as returned in x-pulse-proof. |
| 429 | rate_limited | The key exceeded its requests per minute. Carries retry-after. | Wait that many seconds, then retry. |
| 429 | rate_limit_error (Anthropic) | On /v1/messages a spent question allowance is a 429, not a 402: the Anthropic dialect has no payment status. | Treat as a quota error, not a transient one: retrying will not help until questions are added. |
| 429 | none | A route-level limit (for example 60 per minute on the Gateway and Cortex query routes, 30 on file upload). Produced by the rate limiter, so its body is not one of the shapes above. | Branch on the status and retry-after, not on the body. |
| 500 | server_error (OpenAI), api_error (Anthropic) | The call failed inside Subsidia or at the model provider. | Retry with backoff, at most three times. Persisting? Report the x-pulse-proof id if one was returned. |
| 501 | address_not_supported (OpenAI), invalid_request_error (Anthropic) | A reserved debate: address was used on the Gateway. | Use the conversations API for debates. |
Retrying safely
| Status | Retry? | How |
|---|---|---|
| 400, 401, 403, 404, 413, 501 | No | The request or the key must change first. |
| 402 | No | Not until a quota, plan or ceiling changes. Surface it to a human. |
429 with retry-after | Yes | Sleep for retry-after seconds (at most 60: the window is one minute), then resend. |
429 on /v1/messages, no retry-after | No | Probably a spent allowance. Check Rate limits and quotas. |
| 500, network error | Yes | Exponential backoff with jitter (1 s, 2 s, 4 s), three attempts, then give up. |
async function call(url: string, init: RequestInit, attempts = 3): Promise<Response> { for (let i = 0; i < attempts; i++) { const res = await fetch(url, init) if (res.ok) return res if (res.status === 429 && res.headers.get('retry-after')) { await new Promise((r) => setTimeout(r, Number(res.headers.get('retry-after')) * 1000)) continue } if (res.status >= 500 && i < attempts - 1) { await new Promise((r) => setTimeout(r, 2 ** i * 1000 + Math.random() * 250)) continue } return res // 4xx: the caller decides } throw new Error('unreachable')}OpenAI and Anthropic dialects side by side
| Situation | `/v1/chat/completions` | `/v1/messages` |
|---|---|---|
Empty or missing messages | 400 invalid_request_error | 400 invalid_request_error |
| Question allowance spent | 402 insufficient_quota | 429 rate_limit_error |
Unknown agent: address | 404 model_not_found | 404 not_found_error |
Reserved debate: address | 501 address_not_supported | 501 invalid_request_error |
| Internal failure | 500 server_error | 500 api_error |
| Bad or missing key | 401, native shape | 401, native shape |
Does a failed call cost me a question?
A refusal before the model runs (400, 401, 402, 403, 404) consumes nothing. For a 500, report it with the proof id if one came back.
How do I tell a transient 429 from a spent allowance?
A transient 429 carries a retry-after header and, from a key limit, code: "rate_limited". A spent allowance on /v1/messages has error.type: "rate_limit_error" and no retry-after.