Error handling
September 8, 2026 · View on GitHub
English · Русский
Error handling
See also: index.md (index) · developer-guide.md (quickstart, key issuance, request examples) · security-model.md (localhost, key model, audit) · versioning-and-deprecation.md (semver policy).
Every error response carries an ApiError JSON body:
{
"error": "forbidden",
"message": "Insufficient tier or missing scope",
"required": { "tier": 2, "scopes": ["task:control"] },
"current": { "tier": 0, "scopes": ["task:read"] }
}
required/current (each: tier + scopes) are only populated on 403; every other status omits
them (message alone, or null).
Status codes and machine-readable error codes
The table below is the complete set of error codes this version returns.
| HTTP status | error code | Meaning | Where it comes from |
|---|---|---|---|
400 | bad_request | Malformed request (bad JSON, invalid parameter). | Any operation, request-shape validation. |
401 | unauthorized | Missing or invalid Authorization: Bearer <api-key> — no key, unknown key, or an expired/disabled one. | Auth gate, every operation except ping. |
403 | forbidden | Key is valid but lacks the required scope, or the operation's tier exceeds the key's maxTier. Body includes required/current. | Auth gate. |
404 | not_found | Three distinct origins, same code: (a) the path/verb resolves to no known operation; (b) on the ZennoPoster host, the operation was resolved and authorized, but the task, instance, tab or element it addresses doesn't exist — e.g. GET /tasks/{id} with an unknown id; (c) on the ProjectMaker host, the action, variable, list, table or spreadsheet the call names doesn't exist in the open project, or the file passed to POST /projects/open doesn't exist. "No project is open at all" is not a 404 — see project_not_open. | (a) Auth gate; (b) ZennoPoster/Instance domain handlers; (c) ProjectMaker domain handlers. |
409 | project_not_open | Every /projects/current/* operation on the ProjectMaker host needs an open project and the editor has none (e.g. right after POST /projects/current/close of the last tab in a race, or before any project was opened). Open or create a project first (POST /projects/open, POST /projects). | ProjectMaker domain handlers. |
409 | failed_precondition | The open project is not in a state that allows the call: POST /projects/current/close with unsaved changes and no discardUnsavedChanges: true, or while the project is running/being debugged; POST /projects/current/save when the target file exists and the user declined to overwrite it. | ProjectMaker domain handlers. |
404 | session_not_found | GET/POST /api/v1/sessions/{id} — no open WaitForUserAction window with that id. | Sessions domain, ZennoPoster host only. |
409 | no_active_interaction | POST /sessions/{id}/complete targets a WaitForUserAction window that is no longer open — already completed, or the window/task closed. instance:interact calls do not return this: driving tabs/DOM needs no open session. | Sessions domain, ZennoPoster host only. |
409 | session_expired | Declared in the contract for a session whose window closed before complete was called. Not returned by this version — treat it as reserved, not as a code you will actually see in v1. | Sessions domain (reserved). |
409 | dom_unavailable | GET /instances/{id}/tabs/{tabId}/dom — the DOM text couldn't be retrieved right now (e.g. the page is navigating). Retry rather than treat as permanent. | Instance domain, ZennoPoster host only. |
409 | instance_busy | DELETE /instances/{id} — the port belongs to a running task's worker thread and cannot be released via the API. | Instance domain. |
409 | instance_view_protected | POST /instances/{id}/show — the browser's view is protected (view protection enabled and no open WaitForUserAction window), so the window cannot be revealed. | Instance domain. |
409 | task_scheduler_owned | DELETE /tasks/{id} — the task is owned by a scheduler job and cannot be deleted directly; delete the scheduler job instead. | Tasks domain, ZennoPoster host only. |
413 | payload_too_large | Request body exceeds the host's upload limit (default 2 GB — a safety cap, not something normal usage hits). Applies to any operation with a JSON body on the ZennoPoster host. | ZennoPoster host, global request-body guard (not operation-specific business logic). |
429 | rate_limited | Too many concurrent GET /sessions/events long-polls (per-host cap, default 32); also reserved for remote-mode rate limiting. | Sessions events long-poll; remote/TLS mode (not yet enabled). |
500 | internal_error | Unhandled server-side failure. | Any operation. |
501 | not_implemented | The path/verb is a declared operation that isn't wired to a handler on this host (e.g. the human-confirmation confirmations_* trio). | Auth gate. |
503 | service_unavailable | The AI/PublicApi runtime master-switch is off — every call is refused before the key is even checked. | Auth gate, checked before key/operation detail. |
A note on 409
The auth gate itself never returns 409: it only ever produces 401 / 403 / 404 / 501.
409 is a domain-level status, returned only by the sessions/instance domain when the call is
well-authorized but the runtime context it needs (an open interaction window) isn't there. Don't conflate the two: a 403 means "your key can't do this"; a 409 means
"your key can do this, but not right now".
Handling checklist for an integrator
401— the key is missing, wrong, expired, or was revoked. Re-issue a key via the UI; there is no refresh flow (opaque keys, no token exchange).403— readrequiredvscurrentin the body and either request a key with the missing scope/tier, or don't attempt the call. Don't retry as-is; it will never succeed with the same key.404/409on the sessions/instance domain — these are expected, not exceptional: aWaitForUserActionwindow can close between yourGET /sessionslisting and yourPOST /sessions/{id}/completecall. Re-list and confirm the session is still open before retrying.429— back off; only relevant once remote mode ships (not in this release).501— the operation is declared in the contract but not wired on this host (isAvailable: falsein/capabilitiesfor thatoperationId); don't call it.503— the AI/PublicApi master-switch is off host-side; nothing will succeed until it's turned back on. Not something a client can work around.409 dom_unavailable— transient; retry the DOM-text call rather than treating it as a permanent failure.413— your request body exceeds the host's upload limit; this is a safety cap (default 2 GB), not a normal-usage limit — if you hit it, something is likely wrong client-side.- Always check
GET /api/v1/capabilitiesfirst (see developer-guide.md) to avoid triggering403/501s you could have predicted client-side.