asssistive

Errors

Every status code the Public API returns, what causes it, and what your integration should actually do about it.

This covers the Public API — the six endpoints you write code against. (The widget handles its own errors; there's nothing for you to catch.)

Check success rather than the HTTP status alone, and branch on error.code rather than matching on error.message — the message is written for a human reading a log, and it can be reworded.

{
  "success": false,
  "request_id": "6d7979ce52075b24979cd5b415319339",
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "the request contains invalid fields",
    "details": [
      { "field": "message", "message": "message is required" }
    ]
  }
}

Log request_id on every failure. It's the correlation ID for that exact request, and quoting it in a support conversation is the difference between "something failed yesterday" and someone finding the request in seconds.

The codes

HTTPerror.codeWhen
400BAD_REQUESTMalformed input — e.g. the conversation ID in the path isn't a valid UUID
401UNAUTHORIZEDMissing, invalid, or revoked publishable key; missing or invalid visitor token
404NOT_FOUNDArticle not found; conversation not owned by the presented visitor token
422VALIDATION_ERRORThe body failed validation — see error.details
429RATE_LIMITEDA rate limit was exceeded
500INTERNALUnexpected server error
503UNAVAILABLEA required dependency is unavailable

403 FORBIDDEN and 409 CONFLICT exist in the platform's error vocabulary but are not reachable on these six endpoints. If you're writing an exhaustive switch, you don't need arms for them.

What to do about each

400 BAD_REQUEST

A client bug, essentially always. Retrying won't help — the request is malformed and will stay malformed.

In practice this is nearly always a path UUID that isn't one: the conversation id is not a valid uuid. Fix the caller.

401 UNAUTHORIZED

The credential is missing, wrong, expired, revoked, or the wrong kind. The API never tells you which, on either credential — and that's deliberate, so a credential's state can't be probed.

For a publishable key, a revoked key, a key that never existed, and an sk_ key all fail identically with the api key is invalid or has been revoked. Check the key's prefix and status in the dashboard; the response won't narrow it down.

For a visitor token, an unknown token, one that has expired, and one an agent has revoked all fail identically with the visitor token is invalid.

Don't write three branches for that — there's one. The token no longer works, so start a new conversation and store the new token. Don't retry the original request; nothing about it will change.

404 NOT_FOUND

Read this as "not yours, or not visible to you" — not as "gone."

A visitor token presented against a conversation it doesn't own returns 404, never 403. That's deliberate: a 403 would confirm the conversation exists, turning a valid token into a way to probe which conversation IDs are real.

The same collapsing happens on articles. article not found covers three cases at once: the article doesn't exist, it exists but isn't published, or it's published but internal. All three mean you may not read this, so all three should render the same page in your UI.

The practical debugging note: a 404 on something you know exists almost always means you're using the wrong credential, not that the record is missing.

422 VALIDATION_ERROR

The request was well-formed but the body didn't validate. error.details is a list of {field, message} pairs, and the field names match the JSON field names you sent, so you can map them straight back onto a form:

{
  "success": false,
  "request_id": "6d7979ce52075b24979cd5b415319339",
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "the request contains invalid fields",
    "details": [
      { "field": "email", "message": "must be a valid email address" }
    ]
  }
}

Show the messages against the fields. Don't retry.

429 RATE_LIMITED

Back off, then retry — exponentially. Retrying a rate limit at the rate that caused it just extends the outage.

The two credentials are limited on different axes:

EndpointsCredentialLimitedDefault
Start a conversation, articles, article, categoriespk_Per organization120/min
Read and post messagesvt_Per token60/min

Every publishable key your organization holds shares one budget

Minting a second key for a second integration buys you no extra headroom — the limit is per organization, not per key. One runaway integration can starve every other one.

It's also why a leaked publishable key matters more than the name suggests: whoever has it can burn your whole organization's quota. Treat it as at least semi-trusted and keep it server-side.

Visitor-token endpoints are limited per token, so each conversation has its own budget and one client cannot starve another. But 60/min is one request per second — a per-second poll sits exactly on the limit, and any retry or jitter tips it over. Poll every 3–5 seconds while the chat is open. See the polling strategy.

Read the headers, not this page. X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (seconds until the window resets) come back on the response and are the source of truth; the defaults above are configurable per deployment.

The limiter is GCRA-based, so it refills smoothly rather than resetting on a hard window boundary — you won't see a cliff where a whole minute's budget reappears at once. And it fails open: if the limiter's backend is unavailable, requests are allowed rather than rejected.

500 INTERNAL and 503 UNAVAILABLE

Not your fault, and — unlike everything above — worth retrying.

Retry with exponential backoff and a jittered delay. 503 means a dependency is temporarily unavailable, which is exactly the shape of problem that resolves on its own within seconds.

If a 500 reproduces consistently for the same request, that's a bug worth reporting. Include the request_id.

A retry policy that works

CodeRetry?How
400, 401, 404, 422NoThe request will fail identically. Fix the caller.
429YesExponential backoff, honoring X-RateLimit-Reset.
500, 503YesExponential backoff with jitter, capped attempts.

On this page