API & Error Codes
Error codes and rate limits
Xisu AI error codes and rate limits: the next step for each code, why low balance shows as rate_limit_error on the Anthropic protocol, backing off by Retry-After on 429, insufficient versus negative balance, and why reports need a request_id.
Almost every failure you hit while integrating is in the table below. None of them means "the service is broken" — each has a definite next step.
| Code | Meaning | What to do |
|---|---|---|
| invalid_api_key | Invalid key | Check it wasn't revoked and was copied in full. If this key has an IP allowlist, calling from elsewhere shows this exact same error — check your source IP first |
| insufficient_balance | Insufficient balance | Top up |
| account_overdrawn | Balance is negative, usually after a refund clawback | Contact support to reconcile the account |
| budget_exceeded | This key hit its budget cap | Raise the budget or use another key |
| model_not_allowed | This key cannot call this model | Read the message: “…by your plan” means upgrade the plan; “…for this API key” means the plan is fine — ask support to widen this key |
| rate_limit_exceeded | Too many requests | Back off per Retry-After |
| no_available_channel | That model is temporarily unavailable | Try again shortly |
| upstream_error | The upstream returned an error, and the cause does not trace back to your payload | Try a different model first; this one is usually on us to fix upstream. A genuinely invalid payload comes back as upstream_rejected_request (400) — but this 502 is still no proof your payload is fine, so if it keeps failing, check it against the message. If it keeps happening, open a ticket with the request_id |
| upstream_rejected_request | The upstream judged the request itself invalid — this one really is the payload | Fix the payload and send it again; retrying it unchanged will fail exactly the same way. The message usually quotes the upstream verbatim — go by that. The most common cause is a tool/function JSON Schema the upstream refuses |
| system_not_supported_upstream | Every upstream currently serving this model drops the system prompt | Retry without the system prompt — that will go through; or pick a model without the “no system prompt” tag on the Models page |
| context_length_exceeded | Input exceeds the context limit declared for this model on our platform | Shorten the input or split it up; a model with a larger context also works. Retrying will not help — the error message carries the actual length and the limit. |
| content_policy_violation | The content was blocked by the platform's content policy | Retrying as-is will always give the same result — change the content and send again; if you believe it is a false positive, open a ticket with the request_id |
| content_policy_needs_text | The request contains content the platform cannot read as text (pre-tokenized token arrays, images, PDFs, etc.) | Send it as text and try again; if you really need to send this kind of content, open a ticket to have this key allowed |
| tenant_closing | Account is being closed | Contact support — topping up will not restore access |
/v1/messages uses a different set of error types
The Anthropic-native protocol has its own error types (error.type in the response body). They do not overlap with the table above — and error.type is what the SDK reads.
| error.type | HTTP | Meaning | What to do |
|---|---|---|---|
| invalid_request_error | 400 | The request was judged invalid (your payload, an over-long input, an upstream refusal, or content the platform cannot read as text — they all land on this one type) | Fix the payload and send it again rather than retrying it unchanged — every one of those causes has the same next step. Read the message: it says which one, and carries our request_id |
| authentication_error | 401 | Invalid key | Check whether it was revoked and that you copied it in full; if the key has an IP allowlist, calling from anywhere else returns this same message |
| permission_error | 403 | Your plan or this key does not allow the model, the content was blocked by the content policy, or the account is being closed | Read the message: if it is about the model, adjust the key's model scope or upgrade the plan; if it is about the content policy, change the content (if you believe it is a false positive, open a ticket with the request_id); if it is about the account, contact support |
| not_found_error | 404 | This model is not on /v1/messages (it has no Anthropic upstream configured) | Pick a different model, or switch to /v1/chat/completions — the message points the way. Retrying will not help |
| request_too_large | 413 | Request body too large | Compress or split the request (multimodal content is usually the bulk of it) |
| rate_limit_error | 429 | Rate limited / quota exhausted / balance too low / key budget reached (all four share this one type) | Check the balance first, then read the message: if it says this is an account limit, retrying will not help; back off and retry only when it really is rate limiting |
| api_error | 502 / 504 | Every upstream failed, or the request was hard-stopped at the timeout | Retry later; if it keeps happening, open a ticket with the request_id |
| overloaded_error | 503 | The model has no usable upstream right now (cooling down or held back) — this is not "no such model" | Back off by the Retry-After in the response and retry — this one is temporary |
⚠️ The one that gets misread is rate_limit_error: an empty balance is reported as that too (the Anthropic protocol has no "insufficient balance" type). On a 429, check the balance first; the message says whether it is an account limit — and if it is, retrying will not help however many times you try.
How to back off when rate limited
A rate_limit_exceeded response carries Retry-After (in seconds). Back off by that value; don't retry on a fixed interval — at peak times a fixed interval just makes you compete with yourself for quota.
Exponential backoff with jitter is the sane default; cap it at whichever is larger, Retry-After or your own timeout.
Include the request_id when you report a problem
Every response carries a request_id. Without it we cannot find that call — the gateway handles a large volume every day, and "the one around ten-ish" doesn't locate anything.
When you open a ticket, include: the request_id, when it happened, which model, and the complete error body.
One case that is very easy to misread
insufficient_balance (not enough balance) and account_overdrawn (negative balance) are not the same thing:
- Not enough balance: the balance is still positive, just short for this call. Topping up genuinely fixes it.
- Negative balance: usually a refund claw-back pushed the balance below zero. Topping up does not address the cause — open a ticket and have support reconcile the account.
These two used to be reported as one code. The result was that customers whose balance had been pushed negative kept topping up, while the actual accounting problem went unlooked-at.