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.

JSON
{
  "error": {
    "type": "invalid_request",
    "code": "assistant_not_published",
    "message": "This assistant is not published."
  },
  "request_id": "req_01HB"
}
typeThe broad category. Branch on this.
codeThe specific cause. Log this, and branch on it where you need to.
messageHuman-readable. Safe to show a person, though usually you will want your own wording.
request_idAlso on the X-Request-Id header of every response, success or failure. Quote it when asking us about a request.

Status codes#

400invalid_requestMalformed JSON, or a header we could not read.
401authentication_errorMissing, unknown, revoked or expired key.
403permission_errorValid key, but not for this resource.
404not_foundNo such resource in this workspace.
409conflictThe request is valid but the resource is in the wrong state.
422invalid_requestWell-formed, but a field is wrong.
429rate_limit_errorToo many requests. See Rate limits.
500api_errorOur fault. Retry.
503service_unavailableA dependency is unavailable, or the assistant is misconfigured.

Codes#

Authentication#

missing_api_keyNeither Authorization: Bearer nor X-Api-Key was sent.
invalid_api_keyThe key does not exist. Usually a truncated copy, or a whitespace character on the end.
revoked_api_keyRevoked in Studio. Create a new one.
expired_api_keyPast 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_permittedThe key is restricted and this assistant is not on its list.

Not found#

assistant_not_foundUnknown 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_foundUnknown session, one from another workspace, or one that has aged out of retention.

Conflict#

assistant_not_publishedPublish it in Studio. Drafts do not answer over the API — why.
assistant_disabledSwitched off in Studio.
session_endedThe conversation is closed. Open a new session.
session_expiredIt ran past max_call_seconds. Open a new session.
turn_in_progressA turn is already streaming on this session. Wait for it, or abort it.
voice_disabledVoice is switched off on this assistant. Read channels.voice before showing a microphone.

Validation#

invalid_requestA field is missing or the wrong type. The message names it.
missing_visitor_fieldsRequired pre-chat fields were not supplied.
message_too_longOver 8000 characters.
text_too_longSpeech text over 1000 characters.
invalid_rangeA usage window that is backwards, or longer than 90 days.

Service#

assistant_unavailableThe assistant cannot answer — usually a model credential that was deleted or is failing. Check Credentials in Studio.
voice_unavailableVoice is on but there is no voice credential, or the provider rejected the request.
api_errorSomething 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:

HTTP
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#

429Retry, after Retry-After. See Rate limits.
500 api_errorRetry with backoff. Up to three attempts.
503 api_errorRetry with backoff.
503 assistant_unavailableDo not retry. Configuration is wrong; it will fail identically.
4xxDo not retry. Nothing about the request will change.
TypeScript
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#