Skip to content

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

StatustypeWhat to do
401authentication_errorThe key is missing, malformed or revoked. Check the Authorization header.
402payment_requiredThe workspace cannot currently be charged for this request.
403permission_errorThe credential is valid but not authorized for this route. Check the key scope and its workspace.
404not_found_errorThe route or the addressed resource does not exist.
429rate_limit_errorBack off and retry. Honour Retry-After when it is present.
Other 4xxinvalid_request_errorThe request was malformed. Fix the payload rather than retrying it.
5xxapi_errorA server-side failure. Retry with backoff.
Status codes and the type each produces.

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.

codeStatusCause
invalid_plugins400plugins was sent and did not decode as an array.
unsupported_parameter400plugins named something this endpoint does not implement.
web_search_not_supported400Hosted search cannot be served for this request.
invalid_web_search_options400web_search_options failed validation.
web_search_output_budget_too_small400The output cap is below the minimum this entry needs to search and still answer.
unsupported_reasoning_effort400reasoning.effort is outside the ladder this entry accepts.
image_detail_unsupported400An image arrived whose size cannot be measured before processing. Send it inline with detail low or high.
model_not_allowed403The key is restricted to an explicit list of models and this one is not on it.
model_not_found403The model is in the catalog but has been deactivated.
insufficient_tier403The workspace tier is below the model's minimum.
server_error403The access check could not complete. Retry.
quota_exceeded429The usage limit for this model has been reached.
web_search_quota_exceeded429The hosted-search usage limit has been reached.
model_unavailable503The model is known but cannot currently be served.
quota_unavailable503Usage quota is temporarily unavailable. Retry.
Every string code, with the status it arrives with. Complete.

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.