Errors
The error envelope, the type values, and how to handle each class.
The error envelopePermalink to The error envelope
Every error response uses the same envelope. Branch on type rather than parsing message, which is written for people and may change.
Error response
{
"error": {
"message": "authentication required",
"type": "authentication_error",
"code": 401
}
}code is not always a number
code carries the HTTP status for most errors, but some responses replace it with a machine-readable string such as model_not_found. Treat it as either a number or a string, and never assume it parses as an integer.
Error typesPermalink to Error types
| Status | type | What to do |
|---|---|---|
| 401 | authentication_error | The key is missing, malformed or revoked. Check the Authorization header. |
| 402 | payment_required | The workspace cannot currently be charged for this request. |
| 403 | permission_error | The credential is valid but not authorized for this route. Check the key scope and its workspace. |
| 404 | not_found_error | The route or the addressed resource does not exist. |
| 429 | rate_limit_error | Back off and retry. Honour Retry-After when it is present. |
| Other 4xx | invalid_request_error | The request was malformed. Fix the payload rather than retrying it. |
| 5xx | api_error | A server-side failure. Retry with backoff. |
String codesPermalink to String codes
When a refusal has a more specific cause than its status conveys, code carries a stable string instead of the status number. These are the string codes the documented endpoints can return. They are complete as of the pinned backend commit, and each is stable — branch on them.
| code | Status | Cause |
|---|---|---|
| invalid_plugins | 400 | plugins was sent and did not decode as an array. |
| unsupported_parameter | 400 | plugins named something this endpoint does not implement. |
| web_search_not_supported | 400 | Hosted search cannot be served for this request. |
| invalid_web_search_options | 400 | web_search_options failed validation. |
| web_search_output_budget_too_small | 400 | The output cap is below the minimum this entry needs to search and still answer. |
| unsupported_reasoning_effort | 400 | reasoning.effort is outside the ladder this entry accepts. |
| image_detail_unsupported | 400 | An image arrived whose size cannot be measured before processing. Send it inline with detail low or high. |
| model_not_allowed | 403 | The key is restricted to an explicit list of models and this one is not on it. |
| model_not_found | 403 | The model is in the catalog but has been deactivated. |
| insufficient_tier | 403 | The workspace tier is below the model's minimum. |
| server_error | 403 | The access check could not complete. Retry. |
| quota_exceeded | 429 | The usage limit for this model has been reached. |
| web_search_quota_exceeded | 429 | The hosted-search usage limit has been reached. |
| model_unavailable | 503 | The model is known but cannot currently be served. |
| quota_unavailable | 503 | Usage quota is temporarily unavailable. Retry. |
Messages are deliberately generic on some refusals
Several of these carry a short, fixed message that says less than the code does, because the detail behind the refusal is not the caller's to see. The code is the part that is stable and specific — read that, and do not try to classify a failure by matching on message text.
Handling errorsPermalink to Handling errors
JavaScript — branch on type, retry only what is retryable
async function callRelane(body, attempt = 0) {
const res = await fetch(class="tk-str">"https:class="tk-commentclass="tk-str">">//api.relane.ai/v1/chat/completions", {
method: class="tk-str">"POST",
headers: {
Authorization: class="tk-str">`Bearer ${process.env.RELANE_API_KEY}`,
class="tk-str">"Content-Type": class="tk-str">"application/json",
},
body: JSON.stringify(body),
});
if (res.ok) return res.json();
const { error } = await res.json();
class=class="tk-str">"tk-comment">// Retryable: transient by nature.
if ((error.type === class="tk-str">"rate_limit_error" || error.type === class="tk-str">"api_error") && attempt < 3) {
const retryAfter = Number(res.headers.get(class="tk-str">"Retry-After")) || 2 ** attempt;
await new Promise((r) => setTimeout(r, retryAfter * 1000));
return callRelane(body, attempt + 1);
}
class=class="tk-str">"tk-comment">// Everything else is a caller problem: retrying sends the same broken request again.
throw new Error(class="tk-str">`${error.type} (${error.code}): ${error.message}`);
}Do not retry a 4xx other than 429
An authentication, permission or invalid-request error will fail identically on a retry. Fix the request instead.