Errors
Every error code with its HTTP status, the stable subcode on auth and limit failures, and the dashboard-only codes that never appear on v1.
Errors use the standard envelope — see Response envelope. Branch on error.code (and, on the auth and limit failures, the finer error.subcode), never on the message text. Every error body also carries a requestId (returned as the X-Request-Id header too) — a safe, non-secret correlation id you can quote to support to find the exact failed request.
Codes and statuses
| Code | HTTP | Meaning |
|---|---|---|
UNAUTHORIZED | 401 | Missing, malformed, revoked, disabled, or unknown API key |
FORBIDDEN | 403 | Authenticated, but not permitted — API access off for the workspace, a missing scope, or an owner-protected target |
NOT_FOUND | 404 | No such resource in this workspace |
VALIDATION_ERROR | 422 | The request body or parameters were rejected |
RATE_LIMITED | 429 | Too many requests — see Rate limits |
USAGE_LIMIT_REACHED | 402 | Out of credits, over quota, or a spend cap was hit |
FILE_TOO_LARGE | 413 | Upload exceeds the plan's size limit |
UNSUPPORTED_FILE_TYPE | 415 | Not one of PDF, DOCX, TXT, MD, CSV |
SOURCE_PROCESSING_FAILED | 500 | A knowledge source failed to process |
CHATBOT_INACTIVE | 403 | The chatbot is not active |
DOMAIN_NOT_ALLOWED | 403 | The request's origin is not on the chatbot's allowlist |
AI_PROVIDER_ERROR | 502 | An upstream AI provider failed |
INTERNAL_ERROR | 500 | Something unexpected |
error.subcode
The auth, access, and rate-limit failures carry a stable error.subcode next to error.code and error.requestId — a finer discriminator within a code, so you can tell two UNAUTHORIZEDs apart without matching the human message (which may be reworded). It is additive: present on the rows below, absent on every other error, and the code/status never move.
subcode | code / HTTP | Meaning |
|---|---|---|
missing_key | UNAUTHORIZED / 401 | No key on either header |
invalid_key | UNAUTHORIZED / 401 | Malformed, unknown, revoked, or wrong-secret key |
key_disabled | UNAUTHORIZED / 401 | The key is paused (you disabled it) |
api_access_disabled | FORBIDDEN / 403 | API access is not enabled for this workspace |
missing_scope | FORBIDDEN / 403 | The key lacks the endpoint's scope |
rate_limited | RATE_LIMITED / 429 | The per-workspace rate limit was hit |
{
"success": false,
"error": {
"code": "UNAUTHORIZED",
"subcode": "key_disabled",
"message": "API key is disabled.",
"requestId": "9f3b7d4c2a61"
}
}What you can hit on v1 today
v1 is a full read-write surface, so most of the table is live:
UNAUTHORIZED— key problems, including a key you disabled yourself.FORBIDDEN— API access off for the workspace (api_access_disabled), a missing scope (Missing required scope: <scope>.— see Scopes), or an owner-protected target (for example, changing the workspace owner's member row).NOT_FOUND— the resource does not exist in your workspace; another workspace's ids read identically to unknown ones.VALIDATION_ERROR— a rejected body or parameter on any write.USAGE_LIMIT_REACHED— a plan cap on creation endpoints (chatbots, sources, seats, reseller client seats) or an out-of-credit state.FILE_TOO_LARGE/UNSUPPORTED_FILE_TYPE— the knowledge file upload.RATE_LIMITED— the per-plan per-workspace budget or the per-IP flood guard, or an endpoint's own stricter one (exports, crawl, topics, domain verification). See Rate limits.INTERNAL_ERROR— rare, and worth reporting.
The chatbot and domain codes (CHATBOT_INACTIVE, DOMAIN_NOT_ALLOWED, AI_PROVIDER_ERROR) belong to the widget's public endpoints and are listed here because one client library usually handles all of them.
Dashboard-session codes
These five exist in the same catalogue and never appear on /api/v1:
| Code | HTTP |
|---|---|
EMAIL_NOT_VERIFIED | 403 |
ACCOUNT_SUSPENDED | 403 |
TWO_FACTOR_REQUIRED | 403 |
ACCOUNT_DELETED | 403 |
WRONG_PORTAL | 403 |
They describe the state of a signed-in user session or the dashboard login flow, and API keys have neither. In particular, when a workspace owner is suspended or deleted, v1 does not return ACCOUNT_SUSPENDED — it returns the ordinary access-gate 403 with "API access is not enabled for this workspace." (api_access_disabled), so a key holder cannot detect a moderation action.
Handling them
async function apiGet(path) {
const res = await fetch(`https://app.evoriqa.com/api/v1${path}`, {
headers: { Authorization: `Bearer ${process.env.EVORIQA_API_KEY}` },
});
const body = await res.json();
if (body.success) return body.data;
switch (body.error.code) {
case "RATE_LIMITED":
throw new RetryableError(body.error.message); // back off and retry
case "FORBIDDEN":
throw new Error("API access is off for this workspace — check the gate.");
default:
throw new Error(`${body.error.code}: ${body.error.message}`);
}
}Retry RATE_LIMITED with backoff and INTERNAL_ERROR sparingly. Do not retry UNAUTHORIZED or FORBIDDEN — nothing about them changes on a second attempt.
Where to go next
Last updated