Errors and retries

Read the error envelope, look up every error code the API defines, and decide which failures to retry and how long to wait.

A failed request returns a non-2xx status and a JSON error body. Read the HTTP status first, then branch on the body's code: the code is stable, while the wording of the message can change.

The error envelope

{
  "code": "invalid_request",
  "error": "invalid JSON body",
  "request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
FieldMeaning
codeA stable, machine-readable error code. Branch on this.
errorA human-readable message. Log it; do not parse it.
request_idThe id of the failed call, equal to the response's X-Request-Id header. Log it with every failure; see Request IDs and metadata.

Some codes add fields next to these three. Each is optional, and present only on the codes listed:

FieldSent on
tierThe tier a model failure or context check was about: upstream_error, upstream_overloaded, upstream_timeout, model_unavailable, and server_shutting_down on chat, and context_overflow.
reasonWhy: rate or concurrency on rate_limited from chat; why a document's text could not be read on file_not_text.
retry_afterThe Retry-After value in seconds: rate_limited, limits_unavailable, credits_unavailable, headless_concurrency_limit.
reset_atWhen the refusal clears (RFC 3339): budget_exhausted, key_spend_cap_exceeded, workspace_spend_cap_exceeded, credits_exhausted. null on model_not_allowed, since waiting never clears an allowlist.
model, state, window, mira_availableA spend or budget refusal: budget_exhausted, key_spend_cap_exceeded, workspace_spend_cap_exceeded, model_not_allowed. window is monthly or burst; mira_available says whether the same request on mira would be served now.
balance, currencyA headless key's remaining credit, on credits_exhausted.
dimensionThe check that faulted on limits_unavailable: rate or budget.
required_scope, allowed_scopesThe family the key would need and the families it has, on key_scope_forbidden.
required_versionThe Cruxy-Version a request field needs pinned, on invalid_request.
hazard_codesThe blocking classifier codes behind moderation_rejected.
tool, bound, limitThe function and the cap it broke, on invalid_tool_schema. bound and limit also name the cap on file_too_large, and limit is the context window on context_overflow.
file_id, filename, content_type, tokensThe refused file, on file_not_found, file_not_text, file_too_large, and image_not_supported.
vision_tiersThe tiers that read images, heaviest first, on image_not_supported.
measured_bytes, limit_bytesThe size and the cap on input_not_countable.

Ignore fields you do not recognize.

Three surfaces use a different shape:

  • POST /v1/chat/completions uses the OpenAI envelope, {"error": {"message", "type", "param", "code"}}. Its code is the same Cruxy code listed here; type is a coarse category derived from the status (invalid_request_error, authentication_error, permission_error, not_found_error, rate_limit_error, or server_error); param is always null.
  • Device login answers a pending or refused poll with an OAuth-style body; see Get your API key.
  • A POST /api/v2/chat stream that fails after it opens ends with an error event carrying code, error, and request_id.

Handle errors and retry safely

This request sends an Idempotency-Key, so once it succeeds, running it again with the same key replays the stored response instead of calling the model and billing again. The gateway stores only a success, so a retry after an error runs the request again under the same key. The TypeScript version retries the codes the API marks retryable, waiting Retry-After when it is sent, and retries a request that got no response with the same key:

IDEMPOTENCY_KEY=$(uuidgen)
 
# -i prints the status line and headers, including Cruxy-Version and the
# X-RateLimit-* headers. Run it twice: once the first call succeeds, the
# second replays it.
curl -i https://api.cruxy.ai/api/v1/chat \
  -H "Authorization: Bearer $CRUXY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -d '{
    "model": "vaani",
    "stream": false,
    "messages": [
      { "role": "user", "content": "Hello" }
    ]
  }'

Which failures to retry

FailureRetry?How
429 rate_limited, credits_unavailable, headless_concurrency_limitYesWait Retry-After seconds, then retry.
503 upstream_overloaded, model_unavailable, all_models_unavailable, server_shutting_down, limits_unavailableYesBack off, honouring Retry-After where it is sent. The gateway already retried the model before it answered upstream_overloaded or model_unavailable.
500 internal_errorYesBack off. A write may have run, so retry with the same Idempotency-Key where the route takes one.
504 upstream_timeoutOnly if changedThe same request usually times out again. Stream it, or lower max_tokens or reasoning_effort; see Timeouts.
429 budget_exhausted, key_spend_cap_exceeded, workspace_spend_cap_exceeded, credits_exhaustedNot nowThe allowance clears only at reset_at, or when the cap changes. Retry-After counts down to that moment.
422 structured_output_unsatisfiedYesIt may pass on a retry. The attempt was billed; a retry with the same key and body is not billed again.
409 idempotency_conflictYes, adjustedResend the original body to replay the stored response, or send the new body under a new key.
No response at all: a dropped connection or your own timeoutYesRetry with the same Idempotency-Key.
A stream that ended before data: [DONE]YesSend the request again; see Streaming.
502 upstream_error from a model callNoThe model provider rejected the request, and it fails the same way again. From native sign-in it is retryable.
503 count_unavailableNoA fact of the deployment; it does not clear.
Every other codeNoIt describes the request or the credential. Fix the request, the key, or the target first.

Rate limits and budgets explains the difference between a rate limit and a budget, and how to read your remaining headroom.

Timeouts

With stream: false, the gateway makes one attempt at the model, bounded by the tier's deadline:

TierDeadline
kavi90 seconds
vaani90 seconds
mira45 seconds

An attempt that runs past the deadline is cancelled and answered 504 upstream_timeout, and nothing is billed. The gateway does not retry a timeout: a turn that needed longer needs longer again. Send stream: true instead, since a stream may run for up to five minutes, or ask for less with a lower max_tokens or reasoning_effort. Reliability collects the deadlines, retries, and stream checks in one place.

Other upstream failures, such as an overloaded or failing model or a dropped connection, are retried inside the gateway a bounded number of times, with backoff, before it answers 503 upstream_overloaded or model_unavailable. On stream: true that happens only while the stream opens, never after a byte has reached you. A call never moves to another tier once it is sent: model: "auto" picks a healthy tier before the call, and an explicit tier that keeps failing is refused at once with 503 model_unavailable while it cools down.

Back off

  • Honour Retry-After. Every 429 carries it, and so does a 503 limits_unavailable. On rate_limited, X-RateLimit-Reset gives the same moment as an absolute Unix time in seconds. On a budget or spend-cap refusal, Retry-After counts down to reset_at instead, while the X-RateLimit-* headers still describe the per-minute request window.
  • Otherwise back off exponentially, with jitter. Double the wait after each failed attempt and randomize it, so many clients that failed together do not retry together.
  • Cap the attempts and the total wait. A retry budget keeps a persistent failure from turning into a stuck request.
  • Log request_id for every failure, retried or not.

Make retries idempotent

Idempotency-Key makes a retry safe. The first call with a key is served, and when it succeeds its response is stored for 24 hours. A later or concurrent call with the same key and the same body replays that response byte for byte, without calling the model again and without billing again; a concurrent duplicate waits for the first call to finish.

Only a success is stored. An error, a 429 or any 5xx included, releases the key, so a retry with the same key and body runs the request again. Reusing the key is the intended way to retry a 429 or a 5xx.

  • Scope: a key belongs to your API key and to the endpoint. On a conversation append, it belongs to the conversation.
  • Conflicts: the same key with a different body is 409 idempotency_conflict.
  • Large responses: a response over 256 KiB is served but not stored, so a retry runs again. It is still billed once.
  • Length: keys are at most 255 characters.

These endpoints accept it:

  • POST /api/v1/chat, POST /api/v2/chat, and POST /v1/chat/completions
  • POST /api/v1/embeddings, POST /api/v1/rerank, and POST /api/v1/moderations
  • POST /api/v1/conversations/{id}/messages, which stores its 201 and replays it without appending a second message

Error codes

Every code the API defines, grouped by the status it is returned with.

400 Bad Request

The request itself is wrong. Fix it; sending it again unchanged fails the same way.

codeMeaning
invalid_requestMalformed JSON, or a field that is missing or invalid. Carries required_version when the request uses a field that needs a newer Cruxy-Version pin.
context_overflowThe input exceeds the tier's context window.
invalid_schemaA response_format JSON schema is malformed, oversized, too deep, or uses an unsupported keyword.
invalid_tool_schemaA tool's parameters schema exceeds a bound. Adds tool, bound, and limit. The fix belongs in your client, so a retry cannot help.
file_not_textA file named in file_ids cannot be read as text. Names the file; a document that could not be read also carries a reason.
file_too_largeA file named in file_ids exceeds a size cap. Names the file, with bound, limit, and tokens.
image_not_supportedA file named in file_ids on this turn is an image, and the tier serving the turn does not read images. Names the image and the tier, and lists the tiers that do in vision_tiers.
moderation_rejectedThe server-side input moderation gate refused the request's text. Carries hazard_codes. Not billed.
pii_rejectedA memory or rule contains an email address, phone number, or card number. Rephrase without it and retry.
rules_too_largeA ruleset exceeds a size cap. Names the cap.
input_not_countablePOST /api/v1/tokens/count refuses to count this text and returns no count.
invalid_versionThe Cruxy-Version header is not a YYYY-MM-DD date. Answered on every route, before authentication.
unknown_versionCruxy-Version is a well-formed date this server does not support. The message lists the supported dates. Answered on every route, before authentication.
invalid_payloadPOST /api/v1/feedback only: the payload does not match the schema for its type.
provider_not_supportedNative sign-in named a provider that is not supported.
redirect_uri_not_allowedNative sign-in named a redirect URI that is not allowlisted.
scope_unsupportedRetired: never sent. It stays in the vocabulary so the string is never reused.

401 Unauthorized

codeMeaning
unauthorizedThe Bearer credential is missing, malformed, unknown, expired, or revoked, or its holder no longer belongs to the organization it names. Each case gets the same answer, with WWW-Authenticate: Bearer. It is the only 401 code; see Get your API key.

403 Forbidden

The credential is genuine, but it cannot make this call. Waiting never widens a credential, so retrying cannot succeed.

codeMeaning
account_suspendedThe key's owner's account is suspended.
credential_quarantinedThe organization the key belongs to could not be established, so the key is refused rather than trusted. Signing in again does not clear it; contact support.
forbidden_bucketThe credential's billing kind cannot be billed on this route. No key issued today can reach it.
key_scope_forbiddenThe key is not scoped for this endpoint family, or for the memory, rules, or files feature a request field asked for. Names required_scope in the second case. Re-scope or re-issue the key.
model_not_allowedThe key's model allowlist excludes the requested tier.
key_read_onlyThe key may not perform billable operations.
org_forbiddenThe operation needs an organization owner or admin.
workspace_forbiddenWorkspace management needs an organization owner or admin.
logs_forbiddenRequest logs need an organization owner or admin.
rules_forbiddenYou may not read or change this ruleset.
vault_forbiddenYou take part in the vault but lack the permission the operation needs.
conversation_forbiddenThe conversation is in a vault you may read but not write.

404 Not Found

The target does not exist, or you cannot see it. The API gives the same answer for an id that is missing and one you cannot see, so a 404 never confirms that an id exists.

codeWhat was not found
user_not_foundA user.
file_not_foundA file of yours under this key's organization, including one named in file_ids.
request_not_foundA request_id from a call this account made, on POST /api/v1/feedback.
memory_not_foundA memory.
vault_not_foundA vault you take part in.
vault_member_not_foundA vault member.
org_member_not_foundAn organization member.
invitation_not_foundAn invitation addressed to you that is still open.
workspace_not_foundA workspace in your organization.
log_not_foundA request log in your organization.
conversation_not_foundA conversation you can see.
message_not_foundA message in the conversation.
prompt_not_foundA prompt in your workspace.
prompt_version_not_foundThat version of the prompt.
eval_suite_not_foundAn eval suite in your workspace.
eval_run_not_foundAn eval run in your workspace.
eval_case_not_foundA case in that eval suite.

409 Conflict

The request conflicts with existing state. Only idempotency_conflict has a retry path.

codeMeaning
idempotency_conflictThe Idempotency-Key was reused with a different body. Resend the original body to replay the stored response, or use a new key.
vault_name_existsA vault with this name already exists in your organization.
vault_external_id_conflictThe external_id names a vault somebody else owns.
vault_member_existsThe user is already a member of the vault.
org_member_existsThe user is already a member of the organization.
org_member_activeThe member has not left the organization.
workspace_slug_existsA workspace with this slug already exists.
workspace_not_emptyThe workspace holds, or has held, a credential, so it cannot be deleted.
workspace_is_defaultThe default workspace cannot be deleted.
prompt_slug_existsA prompt with this slug already exists.
eval_suite_slug_existsAn eval suite with this slug already exists.

413 Payload Too Large

codeMeaning
payload_too_largeThe upload or message is over its size cap.
storage_quota_exceededThe upload would take your files past your total storage allowance.

422 Unprocessable Content

codeMeaning
structured_output_unsupportedThe tier that served the request does not support constrained decoding for response_format.
structured_output_unsatisfiedThe model's non-streaming output did not satisfy the requested JSON schema. The request is still billed.

429 Too Many Requests

Every 429 carries Retry-After, and nothing is billed.

codeMeaning
rate_limitedToo many requests: reason is rate (the per-minute request window) or concurrency (too many of your requests in flight at once). Retry after Retry-After.
budget_exhaustedA signed-in plan's monthly or 12-hour burst pool is spent, or mira's monthly request cap. mira_available says whether the same request on mira would be served now. Clears at reset_at.
key_spend_cap_exceededThe key's monthly spend cap is spent. Clears at reset_at.
workspace_spend_cap_exceededThe workspace's monthly spend cap is spent. Clears at reset_at.
credits_exhaustedA headless key's credit pool is drained. Clears at reset_at.
credits_unavailableA headless key's credit balance could not be read. Retry after Retry-After (2 seconds).
headless_concurrency_limitChat only: a headless key already has its maximum requests in flight. Retry after Retry-After (1 second).

5xx Server errors

On chat, every model failure adds tier, and none is billed.

codeStatusMeaning
internal_error500An unexpected server fault, on any route. Retry with backoff.
upstream_error502On a model call, the model provider rejected the request the gateway sent, and a retry fails the same way. On native sign-in, the identity provider could not be reached or answered unusably; retry.
upstream_overloaded503The model is overloaded, and the gateway's own retries ran out. Retry with backoff.
model_unavailable503The tier is temporarily unavailable: it kept failing and is cooling down, or it failed again on this request after the gateway's retries. Retry with backoff.
all_models_unavailable503Chat only: model: "auto" was sent and every tier is unavailable. Retry with backoff.
server_shutting_down503The gateway restarted, for a deploy, before it answered. Retry with backoff.
limits_unavailable503A rate-limit or budget check could not be read, so the request was refused rather than served unmetered. Adds dimension. Retry after Retry-After (5 seconds).
count_unavailable503POST /api/v1/tokens/count cannot count for this tier, because the count could disagree with billing. It does not clear on a retry.
upstream_timeout504The model did not answer in time. With stream: false the gateway made one attempt and does not retry it; see Timeouts.

Stream errors

A stream that fails after it opens has already sent its 200, so the code arrives in the stream, not the status:

  • POST /api/v2/chat ends with an error event whose code is one of upstream_error, upstream_overloaded, upstream_timeout, model_unavailable, server_shutting_down, or internal_error.
  • POST /v1/chat/completions ends with a data: {"error": {...}} frame.
  • POST /api/v1/chat sends no error frame: the stream ends without data: [DONE]; see Streaming.

Codes in request logs

client_closed_request (499) records that the caller disconnected before the gateway had an answer. Nobody receives it; it appears only in request logs.

Next steps

Was this page helpful?