Errors
Every error code with its HTTP status, which ones the read and write surfaces can return, and the dashboard-only codes that never appear on v1.
Errors use the standard envelope — see Response envelope. Branch on error.code, 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 — the access gate, 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 |
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— the layered access gate, 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 two 120-per-minute limits, or an endpoint's own stricter one (exports, crawl, topics, domain verification).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.", identical to an entitlement gap, 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