Errors
Every Sevren endpoint answers errors in the OpenAI format with a stable code, except the Anthropic-compatible routes, which use Anthropic's format. Messages are short and fixed; they never repeat your request content.
The error envelope
HTTP/1.1 402 Payment Required
Content-Type: application/json
{
"error": {
"message": "The account's remaining credit cannot cover this request.",
"type": "insufficient_quota",
"code": "insufficient_quota"
}
}Branch on code, not on message: messages can be reworded, codes do not change. type follows the HTTP status:
| Status | type |
|---|---|
| 400, 404, 409, 413 | invalid_request_error |
| 401 | authentication_error |
| 402 | insufficient_quota |
| 403 | permission_error |
| 429 | rate_limit_error |
| 500 to 504 | server_error |
During a stream the status is already 200, so errors arrive as an event, data: {"error": {…}}, followed by data: [DONE]. Check each event for an error key.
Request ids
Every response from POST /v1/chat/completions, POST /v1/messages and POST /v1/messages/count_tokens, successful or not, carries an X-Request-Id header such as req_3f9a… (req_ and 32 hex digits). It is random and identifies nothing but that response. Quote it when you report a problem.
When one of these requests answers 400, Sevren keeps it for up to 7 days so the error can be debugged: the request body (up to 64 KiB), the error answer and, if the model refused the request, the model's answer, under that request id. No IP address, API key or account is stored with it. Requests that do not get a 400, and requests the child-safety screen refuses, are never stored. See Privacy.
Model API
POST /v1/chat/completions and GET /v1/models on api.example.com. POST /v1/messages and POST /v1/messages/count_tokens answer the same statuses in Anthropic's shape, {"type": "error", "error": {"type", "message"}}, without the code; see Anthropic API.
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | The body could not be read, the conversation could not be formatted for the model, or the model backend rejected the request. |
| 400 | invalid_json | The body is not one JSON object. |
| 400 | invalid_model | No model, or an id longer than 128 characters. |
| 400 | invalid_messages | messages is missing, empty or has no user message; a tool message has no tool_call_id; or a tool call is malformed. |
| 400 | too_many_messages | The conversation has more messages than allowed. |
| 400 | invalid_role | A role other than system, user, assistant, tool or developer. |
| 400 | invalid_content | Content is not a string or text parts, or not valid UTF-8. |
| 400 | unsupported_content | A non-text part: images, audio or files. |
| 400 | invalid_tools | tools is not an array of function tools, has more than 128, repeats a name, or has a name or parameters schema that is not valid. |
| 400 | invalid_tool_choice | tool_choice is not "none", "auto", "required" or a declared function. |
| 400 | unsupported_parameter | A field such as logprobs, n > 1 or the legacy functions with a value other than its no-op value. |
| 400 | invalid_parameter | A sampling field or max_tokens out of range or of the wrong type. |
| 400 | context_length_exceeded | The prompt does not fit the model's context window. |
| 400 | content_blocked | Refused by the child-safety screen. |
| 400 | input_too_long_for_screening | The input is too long for the child-safety screen to check. |
| 401 | key_required | A paid model was called without a session token or API key. Sign in or use an API key. |
| 401 | invalid_api_key | The key is unknown, or the session token is expired or revoked. |
| 401 | key_inactive | The account is suspended. A request that is running when its account is suspended or its key is revoked also ends with this code. |
| 402 | insufficient_quota | Free tokens and credit cannot pay for the prompt and one output token. |
| 403 | entitlement_required | A gated model was called without a valid entitlement. |
| 403 | model_not_allowed | The account may not use this model. |
| 403 | free_limit_reached | A free question from the web terminal without an account has a prompt that is too long. Sign in to continue. |
| 404 | model_not_found | Unknown or disabled model id. |
| 413 | request_too_large | The body is over the size limit. |
| 429 | rate_limited | The model's request limit was reached. Retry-After is set. |
| 429 | free_limit_reached | Today's free questions without an account are used up. Retry-After is set to the next 00:00 UTC. |
| 502 | upstream_error | The model backend failed. |
| 503 | model_unavailable | No healthy backend for the model. |
| 503 | upstream_unavailable | The model backend is overloaded. |
| 503 | guard_unavailable | The child-safety screen is unavailable; requests are refused until it is back. |
| 503 | unavailable | A database or other dependency is unavailable. |
| 503 | maintenance | The operator switched on maintenance. The message is the operator's notice; Retry-After: 60. |
| 504 | model_not_responding | The model did not start answering within the first-token limit (180 seconds). Nothing is charged, from credit or free tokens; retry or choose another model. |
| 504 | timeout | The request ran longer than the server allows. |
Account API
Routes under /account on api.example.com.
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | The body is not valid JSON, has unknown fields or trailing data. |
| 401 | invalid_session | Missing, unknown or expired session token. Sign in again. |
| 401 | invalid_assertion | The wallet assertion is invalid, expired or already used. Sign again. |
| 409 | wallet_expired | The auth service no longer has the wallet record. Sign in with the wallet again. |
| 413 | request_too_large | The body is over 8 KiB. |
| 429 | rate_limited | A wallet sign-in or plan refresh budget is used up. Retry-After is set. |
| 503 | wallet_unavailable | Wallet sign-in or holder plans are not enabled. |
| 503 | unavailable | The account service could not complete the request. |
Wallet routes
Routes under /auth on auth.example.com.
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | Missing message or signature, or a malformed body. |
| 400 | invalid_message | The text is not a valid EIP-4361 message. |
| 400 | invalid_resource | Not exactly one urn:sevren:wallet: resource, or one other than urn:sevren:wallet:login. |
| 400 | unsupported_signature | A signature type that cannot be verified, such as one from a wallet that is not deployed yet. |
| 401 | unauthorized | Bad signature, domain, chain, statement or age, or a nonce that was already used. |
| 413 | request_too_large | The body is too large. |
| 429 | rate_limited | A service-wide per-minute budget for nonces or verifications is used up. |
| 503 | unavailable | The chain or a database could not be read. |
Rate limits and Retry-After
Limits are counted per model, per account or for the whole service. The one exception is free questions without an account, counted under a keyed pseudonym of the IP address that changes every UTC day (see Privacy):
| What | Limit |
|---|---|
| Token-gated models | A per-hour request budget per holder, set on each gate. |
| Free questions in the web terminal | 7 per UTC day per client without an account, or 25 per signed-in wallet, shared across the free models, plus a service-wide daily budget. |
POST /account/login/wallet | A service-wide per-minute budget. |
POST /account/plan/refresh | 6 per hour per account. |
POST /auth/siwe/nonce, POST /auth/wallet/verify | Service-wide per-minute budgets. |
Retry-After is a number of seconds. The model API sets it to the time left in the current window, the account API to 60, and maintenance responses to 60. The wallet routes do not send it; their budgets reset every minute.
Retrying
- Retry
429,502,503and504: wait forRetry-Afterwhen it is present, otherwise back off exponentially. - Do not retry other
4xxerrors unchanged; fix the request, sign in again or add credit first. - A retried request is a new request and is charged again if it runs.
import time, requests
def post_with_retry(url, **kwargs):
for attempt in range(5):
response = requests.post(url, timeout=320, **kwargs)
if response.status_code not in (429, 502, 503, 504):
return response
wait = response.headers.get("Retry-After")
time.sleep(int(wait) if wait and wait.isdigit() else 2 ** attempt)
return response