Errors
kenari returns errors in the same format as the API you called. Handle an error by its HTTP status and its code, or error.type on Messages, never by the message text. Most messages are in English and a few are in Indonesian, and the wording can change.
Error formats
Section titled “Error formats”OpenAI format
Section titled “OpenAI format”Every endpoint except Messages returns this body: Chat completions, Responses, Embeddings, Rerank, Moderations, Images, audio, music, videos, OCR, web and X search, models and account quota.
{ "error": { "message": "free-model rate limit reached; slow down and retry", "type": "rate_limit_exceeded", "param": null, "code": "free_quota_rpm" }}code is the field to branch on. type repeats code for most errors. The exceptions are the free-model limits (type is rate_limit_exceeded), provider rejections (type is invalid_request_error), shared_key_not_allowed, restricted_key_not_allowed and cli_login_key_required (type is permission_error) and an internal failure on GET /v1/account/balance, GET /v1/account/quota and the Usage endpoints (type is api_error, code is internal_error). param is always null, except on a 400 from the Usage endpoints, where it names the parameter.
Anthropic format
Section titled “Anthropic format”Messages, including count_tokens, returns Anthropic-style errors. There is no code field, so branch on the HTTP status and error.type.
{ "type": "error", "error": { "type": "rate_limit_error", "message": "free-model rate limit reached; slow down and retry" }}error.type | HTTP status |
|---|---|
invalid_request_error | 400, or the provider’s own status for a provider rejection |
not_found_error | 400, or the provider’s own status for a provider rejection |
payment_required | 402 |
permission_error | 403 |
rate_limit_error | 429 |
api_error | 400, 500 and 503 |
The HTTP status is the same as in the OpenAI format for the same failure. A provider rejection keeps the provider’s own status, so these two types are not always 400. See Provider rejections. api_error arrives with 400 when a provider answered with a client error that kenari did not classify.
Errors in a stream
Section titled “Errors in a stream”After a stream has started, the HTTP status is already 200, so a failure arrives as an event in the stream. Check every stream for these events. See Streaming.
| API format | How the error arrives |
|---|---|
| Chat completions | A data: chunk with an error object whose type is upstream_error |
| Messages | An event: error event with the Anthropic body above |
| Responses | A response.failed event, with the error in response.error |
On Chat completions and Responses, code is stream_interrupted when the provider stopped before the answer was complete. The Messages event has error.type and no code. Send the request again as a new request. A provider rejection in the stream uses the codes under Provider rejections.
Plain-text responses
Section titled “Plain-text responses”Some responses are plain text, not JSON, because the request is rejected before it reaches the API.
| Status | Body | Cause |
|---|---|---|
| 400 | Failed to parse the request body as JSON... | On /v1/web/search and /v1/ocr, the body is not valid JSON |
| 401 | missing authorization | No key header was sent |
| 401 | invalid key | The key does not start with kn-, does not exist, was revoked, has expired, or belongs to an account that is suspended or being deleted |
| 408 | request body timed out | The body stopped arriving for 30 seconds, or took more than 5 minutes to arrive. Send it again |
| 413 | request body too large | The request body is over 32 MiB |
| 415 | Expected request with Content-Type: application/json | On /v1/web/search and /v1/ocr, the Content-Type: application/json header is missing |
| 422 | Failed to deserialize the JSON body... | On /v1/web/search and /v1/ocr, a required field is missing or has the wrong type |
| 429 | too many requests, try again shortly | One client IP sent too many requests on a shared key. See Rate limits. |
| 500 | db error | kenari could not look up the key. Retry. |
| 503 | server busy, try again shortly | kenari is receiving too many large request bodies at once. Retry shortly |
For keys, see Authentication and API keys.
Error codes
Section titled “Error codes”400 Bad request
Section titled “400 Bad request”| Code | When | What to do |
|---|---|---|
bad_request | The body or a parameter is invalid, or the key’s restrictions refuse the request (model not allowed, capability not granted, route on a restricted key). An attached document with an unsupported type, a size or page count over the limit, or a length over the model’s context is also refused here. On Embeddings, Rerank, Moderations and System One, a model that is unknown or of another kind is refused here too, and the message lists the ids that are available. Creating a per-tool key is refused here with key limit reached when your account is at its key limit | Read message, fix the request. |
model_not_found | On Chat completions, Responses and Messages, the model id or route name does not exist, or you cannot use it. On Embeddings, Rerank, Moderations and System One, the model is listed but no provider serves it right now | Pick an id from GET /v1/models, or retry later. |
request_blocked | kenari refused the request under its content policy before it reached a provider | Change the content of the request. |
byok_malformed_model_id | A byok/ model id is not in the form byok/<provider>/<model> | Fix the id. See BYOK. |
byok_provider_not_found | You have no BYOK key for that provider | Add the key, or fix the provider name. |
upstream_error | A provider answered with a client error that kenari did not classify | Check the model and the request fields, or try another model. |
Provider rejections
Section titled “Provider rejections”These codes mean the model’s provider rejected the request itself. The HTTP status is 400, or the provider’s own 403, 404, 413, 415, 422 or 429, and type is invalid_request_error.
| Code | When | What to do |
|---|---|---|
invalid_request_error | The provider rejected the request as malformed or unsupported | Send the same request to another model. If it works there, report it to kenari support. |
context_length_exceeded | The prompt plus max_tokens exceed the model’s context window | Shorten the input, lower max_tokens, or use a model with a longer context. |
model_not_found | The provider does not serve the model right now | Try again later or use another model. |
content_policy_violation | The provider blocked the request under its content policy | Change the content of the request. |
prefill_unsupported | The last message is from the assistant, and the model cannot continue it | End the conversation with a user or tool message. |
upstream_rejected | The provider rejected the request for a reason kenari could not classify | Check the model id and the request fields. |
kenari does not retry a rejected request on the same provider. In a route or a BYOK chain it can move on to the next step, because one provider’s rejection does not prove the next one will reject the same request, but a context length rejection ends the chain. The message is written by kenari and never contains the provider’s own text.
401 Unauthorized
Section titled “401 Unauthorized”The body is plain text. See Plain-text responses. Check the key, or create a new one in the dashboard.
402 Payment required
Section titled “402 Payment required”| Code | When | What to do |
|---|---|---|
insufficient_balance | Your balance cannot cover the request, a key’s spend cap or token cap is reached, or the owner of a shared API key has run out of funds | Top up, or send a shorter prompt or a lower max_tokens. The message shows your available balance and the most this request could cost, which is the input estimate plus max_tokens at the output rate. For a capped key, the owner creates a replacement key with a higher cap and revokes the exhausted one, because a key’s caps cannot be changed. A document read with the ocr engine or /v1/ocr has its own message: it says that document reading is billed per page from your balance and not covered by plans, and shows the balance available and the most the document reserves. Top up, or send the file without the ocr engine so that it is read as ordinary input tokens. |
payg_limit_reached | You reached the PAYG balance limit you set in your subscription settings. It covers a rolling day or 30 days | Raise or remove the limit, or wait for the window to roll. Plan quota and free models are not affected. |
See How billing works.
403 Forbidden
Section titled “403 Forbidden”| Code | When | What to do |
|---|---|---|
subscribers_only | The model is only for accounts with an active subscription | Subscribe, or pick another model. See Subscriptions. |
byok_credential_unavailable | You have BYOK keys for that provider, but none can serve the request right now because they are disabled or paused | Enable a key, or wait for a paused key to recover. |
byok_spend_capped_key | A key with a spend cap called a byok/ model | Use a key without a spend cap. A token cap is allowed. |
shared_key_not_allowed | A shared API key called GET /v1/account/balance, GET /v1/account/quota, a Usage endpoint or a Per-tool keys endpoint | Use a key from the account itself. |
restricted_key_not_allowed | A restricted key called GET /v1/account/balance, GET /v1/account/quota or a Per-tool keys endpoint | Use a key with no model restriction, spend cap or token cap. See Account overview. |
cli_login_key_required | A key other than the one from kenari login called a Per-tool keys endpoint | Use the key from kenari login. Run kenari login again if your key is older than per-tool keys. |
404 Not found
Section titled “404 Not found”| Code | When | What to do |
|---|---|---|
not_found | DELETE /v1/account/keys/{id} got an id that is not an active per-tool key of the calling key | List your keys with GET /v1/account/keys and use an id from it. See Per-tool keys. |
409 Conflict
Section titled “409 Conflict”| Code | When | What to do |
|---|---|---|
child_key_cap | The login key already holds the most per-tool keys it can | Revoke one with DELETE /v1/account/keys/{id}, then create the new key. See Per-tool keys. |
429 Too many requests
Section titled “429 Too many requests”Every 429 code is explained, with Retry-After behavior, in Rate limits.
| Code | When |
|---|---|
rate_limit_exceeded | The account per-minute limit, the usage reads limit, the per-tool key creation limit, a burst of free requests, a free-model token budget, or music concurrency |
free_quota_rpm | The free-model per-minute limit |
free_quota_daily | The free-model daily quota, which resets at 00:00 UTC |
plan_limit_reached | A subscription quota window is used up, and PAYG and the free fallback are off |
upstream_error | A provider rate limited the request |
500 Internal server error
Section titled “500 Internal server error”| Code | When | What to do |
|---|---|---|
internal_error | Something unexpected failed in kenari | Retry once. If it keeps happening, report the request-id header to kenari support. |
503 Service unavailable
Section titled “503 Service unavailable”| Code | When | What to do |
|---|---|---|
upstream_error | A provider timed out, could not be reached, or failed | Retry with backoff. |
all_providers_failed | Every provider, or every step of a route, failed. Retry-After is set when the providers are paused or at capacity | Retry after the Retry-After seconds, or use a route with a fallback step. See Routing and routes. |
no_wire_for_modality | No provider currently serves this kind of request for the model, such as chat, image, edit or embedding | Try again later, or use another model. |
Provider names
Section titled “Provider names”Error messages never name the provider behind a model. kenari writes every message itself.
Long requests
Section titled “Long requests”A non-streaming request that runs for about two minutes with no response can be cut off by the network edge before the answer reaches you. For long prompts, such as more than 100,000 tokens, large max_tokens, or slow reasoning models, set "stream": true. A stream keeps the connection open while the model works. See Streaming.
When a non-streaming request is still running after a long wait, kenari can keep the connection open by sending spaces before the JSON body. JSON parsers ignore the spaces. The status line is already 200 by then, so check the body for an error object before you read the answer.
Handle errors
Section titled “Handle errors”- On OpenAI-format APIs, use the HTTP status and
code. On Messages, use the HTTP status anderror.type. Treatmessageas text for people. - Retry
429withrate_limit_exceeded,free_quota_rpmorupstream_error, and retry500and503. Wait forRetry-Afterwhen it is present, and use exponential backoff with jitter when it is not. - Do not retry other
4xxerrors unchanged. Fix the request, the key or the balance first. - For
402, top up or change the limit that was reached, then send the request again. - Check streams for error events, and do not assume every error body is JSON. See Plain-text responses.
- Log the
request-idresponse header, and include it when you contact kenari support.