Error codes
The API returns errors in a stable, machine-readable envelope. The code field is the contract — never branch on the error message text, that's human-facing copy and changes without warning.
{
"success": false,
"code": "VALIDATION_FAILED",
"error": "Human-readable summary",
"details": { "fieldErrors": { "email": "Already in use" } }
}Updated 2026-06-25 — 66 codes the API can emit. These ship with the API and the SDK. See also: Idempotency, Rate limits, Webhooks.
Auth & identity
The caller is authenticated but their account is still in `unverified` state (or `users.email_verified_at` is null). Emitted by `VerifiedUserGuard` on workspace-scoped endpoints. HTTP 403 — the web client maps this to `/auth/verify-required`, where the user can resend the verification email or change the address they signed up with. Distinct from `AUTH_EMAIL_NOT_VERIFIED` (which Google OAuth raises when the *provider* says the email is unverified — a sign-in-time rejection, not a post-login workspace gate).
The session's sliding idle window has expired. The refresh endpoint returns this when `last_activity_at` is older than the configured IDLE_TIMEOUT (30 min). The web client treats this distinctly from a stale/revoked token — it redirects to login with `?reason=inactive` so the inactivity banner is shown.
The current session is an admin-impersonation session and the requested endpoint is on the deny-list for impersonated principals (e.g. changing the target user's email or password, initiating a paid checkout on their behalf, deleting their account). HTTP 403 — the frontend renders a generic "this action is not available while impersonating" message and surfaces a hint to stop impersonating.
The impersonation `target` rejected by the admin endpoint: either does not exist, is suspended / deleted, is another platform admin (admins can never impersonate each other), OR is the caller themselves. HTTP 400. Distinct from `AUTH_INVALID_CREDENTIALS` so the UI can surface a precise message.
Validation
The same `Idempotency-Key` was replayed with a different request body. Emitted by `IdempotencyService` on cache hit when the persisted `body_hash` does not match the incoming payload's hash. HTTP 422 — the client either has a key-reuse bug or two concurrent generators collided. Either way, the safe action is to mint a new key.
Resources
No additional description.
No additional description.
No additional description.
Workflow / state
No additional description.
No additional description.
No additional description.
No telephony provider is registered for the requested provider id (e.g. `TelephonyRegistry.get('plivo')` before the Plivo adapter is wired). Distinct from `PROVIDER_NOT_CONFIGURED` (the provider exists but the tenant has no credential). HTTP 500 — an unknown provider id in a production path is a wiring bug, not a tenant-actionable error.
No additional description.
No additional description.
The workspace has no `trial` / `active` subscription, so the requested action is gated behind buying or renewing a plan. Emitted by `SubscriptionGuard`. HTTP 402 Payment Required — the web client maps this to a "Choose a plan" upsell.
The workspace's plan does NOT include the feature the requested endpoint gates on (e.g. white-label `branding`, `enterprise_analytics`). Distinct from `SUBSCRIPTION_REQUIRED` (no usable plan at all) — here a plan exists but lacks the specific entitlement. Emitted by `EntitlementGuard` (`@RequireEntitlement(...)`). HTTP 402 Payment Required — the web client maps this to an "Upgrade to unlock" upsell.
Rate limiting
The workspace, API key, or per-resource quota has been exhausted for the current window (per-day budget, per-month plan cap, etc.). HTTP 429. Distinct from `RATE_LIMITED` (instantaneous burst protection) — `QUOTA_EXCEEDED` requires either upgrading the plan or waiting for the next quota window to roll over.
Community / forum
The user has been banned from posting in the forum (thread, reply, reaction, mention). Emitted by `ForumBanGuard`. HTTP 403. The user is still allowed to read public content — the ban only gates writes.
The caller submitted an emoji to the reaction toggle endpoint that is NOT in the operator-fixed `REACTION_CATALOG`. Open-emoji picker is intentionally unsupported in v1 to keep the per-target reaction tallies bounded + the moderation surface predictable. HTTP 422 — the web client surfaces a "this emoji isn't supported" toast.
Workspace-level lifecycle gate. The workspace was suspended by a platform admin (`workspaces.status = 'suspended'`) and the requested action — typically originating an outbound call or accepting an inbound one — is refused while the workspace is in this state. HTTP 403. The web app maps this to a "Workspace suspended" banner.
Per-number lifecycle gate. The DID is `'suspended'` (either by a cascade suspend of the parent workspace OR by a per-number admin suspend). Origination using this number is refused. HTTP 403.
Per-agent lifecycle gate. The agent is `'suspended'` (cascade or per-agent). Origination through this agent is refused. HTTP 403.
Server / dependencies
No additional description.
No additional description.
No additional description.
Operator already has an active human-dialer call.
The destination country of the outbound dial is not enabled for the workspace's outbound calling. The web client maps this to a friendly toast PLUS an inline "Enable <country>" one-click button that POSTs to `/outbound-destinations/:code/enable`. `details.countryCode` carries the ISO-3166 alpha-2 code so the UI can render the button label + lookup the country flag without re-parsing the message. HTTP 422 — actionable by the operator, not a transient upstream failure.
The outbound dial was refused by a provider-neutral telephony guardrail BEFORE the leg connected: the destination is on the workspace Do-Not-Call list, or on the global emergency/system hard-block list. Emitted by `TelephonyGuardrailsService.checkOutbound`. HTTP 403 — the web client maps this to a "This call cannot be placed" toast. `details.reason` is `'dnc'` or `'hard_block'` so the UI can tailor copy without parsing the message.
Live-call listener (Phase 11.5 — v1)
Listener attach requested for a call that's not in an attachable state (queued, no_answer, completed, failed, cancelled, busy). The UI should refresh its live-calls list — the call is no longer live. HTTP 409.
The same user already has an active listener session for this call. Detach the existing session before attaching again. HTTP 409.
Listener attach refused — the workspace-scoped endpoint detected the URL `:id` (workspace) does not own the target `:callId`. A cross-tenant probe; logged + audited. HTTP 403.
Web widgets (embeddable visitor widget — v1)
Widget id was not found (or has been archived). HTTP 404.
Widget is `paused`/`archived` — visitor session start refused. HTTP 409.
Visitor's `Origin` header is not on the widget's allowlist. HTTP 403.
Widget's `max_sessions_per_day` cap is exhausted. HTTP 429.
Visitor exceeded `max_sessions_per_visitor_per_day` on this widget. HTTP 429.
Workspace credit balance can't fund another widget session. HTTP 402.
Session JWT presented by the visitor has expired or is unknown. HTTP 401.
Page Manager (CMS) — platform-admin managed marketing/legal pages.
No matching page row (id, or slug+locale+published) found. HTTP 404.
(slug, locale) pair is already taken by another row. HTTP 409.
Page is archived / suspended / already published — publish refused. HTTP 409.
Platform Help & Support (platform-level ticketing for Voxy's own users).
No matching support ticket for the (ref) or (ref, token) lookup, OR the supplied thread token did not verify. Deliberately collapsed to a single generic code so the public surface gives NO existence oracle — an unknown ref and a wrong token return the identical 404 body. HTTP 404.
A ticket status transition was requested that the state machine does not permit from the current status. Emitted by the admin PATCH path. HTTP 409.
The AI draft-generation service is unavailable OR the draft generation call failed / timed out. The admin UI surfaces a graceful "couldn't draft a reply" toast and leaves the composer untouched. HTTP 503.
Advanced Support (S1/S2/S3)
A referenced support department / category / agent / SLA policy does not exist (or has been archived). Emitted by the admin config CRUD + assignment paths. HTTP 404.
A support department / category / SLA-priority slug collides with an existing row, OR an SLA policy already exists for that priority. HTTP 409.
The presented (ref, email) pair did not resolve to a track session, OR the supplied OTP did not verify. Collapsed to one generic code so the public track surface gives NO existence oracle. HTTP 401.
A live-chat session action was requested in a state the session machine forbids — claiming an already-claimed session, sending to an ended session, requesting chat on a non-open ticket. HTTP 409.
White-label licensing (issuer side + deployed validator)
The signed license failed verification: malformed token, bad base64, or the Ed25519 signature did not verify against the public key. A forged / tampered license. The deployed validator rejects this offline (no network). HTTP 400. Distinct from `LICENSE_EXPIRED` / `LICENSE_REVOKED` (those verify fine but are no longer honoured).
The license `expiresAt` has passed (after any offline grace). HTTP 403.
The license has been revoked or suspended by the issuer. The deployed instance enters its offline grace window, then degrades to read-only — never an abrupt hard-stop. HTTP 403.
Too many active deployments for one license key (seat / activation limit exceeded). HTTP 403.
The platform has no license-signing keypair provisioned (the `license_signing` provider credential is missing). Issuing a license is impossible until an operator seeds the keypair. HTTP 500 — an issuer wiring/config gap, not a caller error.