Reference
Errors
One error shape, every code, and what to actually do about each.
Every failure returns the same shape, so you write one handler.
{
"error": {
"type": "invalid_request",
"code": "assistant_not_published",
"message": "This assistant is not published."
},
"request_id": "req_01HB"
}type | The broad category. Branch on this. |
code | The specific cause. Log this, and branch on it where you need to. |
message | Human-readable. Safe to show a person, though usually you will want your own wording. |
request_id | Also on the X-Request-Id header of every response, success or failure. Quote it when asking us about a request. |
Status codes#
400 | invalid_request | Malformed JSON, or a header we could not read. |
401 | authentication_error | Missing, unknown, revoked or expired key. |
403 | permission_error | Valid key, but not for this resource. |
404 | not_found | No such resource in this workspace. |
409 | conflict | The request is valid but the resource is in the wrong state. |
422 | invalid_request | Well-formed, but a field is wrong. |
429 | rate_limit_error | Too many requests. See Rate limits. |
500 | api_error | Our fault. Retry. |
503 | service_unavailable | A dependency is unavailable, or the assistant is misconfigured. |
Codes#
Authentication#
missing_api_key | Neither Authorization: Bearer nor X-Api-Key was sent. |
invalid_api_key | The key does not exist. Usually a truncated copy, or a whitespace character on the end. |
revoked_api_key | Revoked in Studio. Create a new one. |
expired_api_key | Past its expiry date. |
All four are 401, and none of them distinguishes further — telling a caller
that a key used to exist is telling them something about your workspace.
Permission#
assistant_not_permitted | The key is restricted and this assistant is not on its list. |
Not found#
assistant_not_found | Unknown assistant, or one this key cannot reach. Deliberately not distinguished from "not permitted" for listing endpoints, so a key cannot be used to discover what exists. |
session_not_found | Unknown session, one from another workspace, or one that has aged out of retention. |
Conflict#
assistant_not_published | Publish it in Studio. Drafts do not answer over the API — why. |
assistant_disabled | Switched off in Studio. |
session_ended | The conversation is closed. Open a new session. |
session_expired | It ran past max_call_seconds. Open a new session. |
turn_in_progress | A turn is already streaming on this session. Wait for it, or abort it. |
voice_disabled | Voice is switched off on this assistant. Read channels.voice before showing a microphone. |
Validation#
invalid_request | A field is missing or the wrong type. The message names it. |
missing_visitor_fields | Required pre-chat fields were not supplied. |
message_too_long | Over 8000 characters. |
text_too_long | Speech text over 1000 characters. |
invalid_range | A usage window that is backwards, or longer than 90 days. |
Service#
assistant_unavailable | The assistant cannot answer — usually a model credential that was deleted or is failing. Check Credentials in Studio. |
voice_unavailable | Voice is on but there is no voice credential, or the provider rejected the request. |
api_error | Something failed on our side. Retry. |
Errors during a stream#
Once a stream has started the status is already 200, so a later failure
arrives as an event instead:
event: error
data: {"code":"api_error","message":"Something went wrong on our side."}The stream closes after it. Whatever was generated up to that point is stored — the transcript records the partial turn rather than pretending it did not happen.
Handle both paths. A pre-stream failure is a status code; a mid-stream failure is an event.
Retrying#
429 | Retry, after Retry-After. See Rate limits. |
500 api_error | Retry with backoff. Up to three attempts. |
503 api_error | Retry with backoff. |
503 assistant_unavailable | Do not retry. Configuration is wrong; it will fail identically. |
4xx | Do not retry. Nothing about the request will change. |
async function withRetry<T>(run: () => Promise<T>, attempts = 3): Promise<T> {
for (let attempt = 1; ; attempt++) {
try {
return await run();
} catch (error) {
const retryable =
error instanceof ZeevaaError &&
(error.status === 429 ||
(error.status >= 500 && error.code === "api_error"));
if (!retryable || attempt >= attempts) throw error;
// Exponential, with jitter — without it, everything that failed
// together retries together and the second wave is the same size.
const wait = 2 ** attempt * 250 + Math.random() * 250;
await new Promise((resolve) => setTimeout(resolve, wait));
}
}
}Retrying a POST /sessions creates a second conversation, and retrying a
/chat sends the message twice. Retry the whole turn only if the first attempt
never reached us — a connection error rather than a response.
What errors never contain#
Model names, provider names, internal ids, stack traces, or the contents of
another workspace. An error message is safe to log and safe to show; if you need
more detail than it gives, request_id is what identifies the request to us.
Next#
- Rate limits — the
429in detail - Authentication — the
401s in detail