API error code reference
Every NovaVMS API error response wraps the code + message inside an error object. The code field is the stable contract — match against it, not the HTTP status alone (statuses overlap across causes).
Envelope shape
{ "error": { "code": "VALIDATION_ERROR", "message": "Human-readable detail", "details": { "field": "email", "reason": "missing" } }}code— UPPERCASE_SNAKE constant. Stable contract.message— human-readable; subject to change between releases.details— optional structured per-error fields (validation field names, conflict targets, etc.). Omitted for most cases.
Source: cloud/pkg/apierror/*.go.
There is no request_id field in the v1 response envelope. Server-side log correlation uses the X-Request-Id header if you set one on the request (the server echoes it back); the body does not surface it.
4xx — client errors
| Code | HTTP | Meaning | How to fix |
|---|---|---|---|
VALIDATION_ERROR | 400 | Request body failed validation. details lists the failing field where available. | Fix the input per the endpoint’s reference page. |
UNAUTHORIZED | 401 | Missing, malformed, expired, or signed-by-different-key token. Also returned when refresh-token user-agent mismatch (D-R2). | Re-authenticate. For JWTs, refresh first; for service-account keys, confirm the sa_ prefix and that the SA has not been disabled. |
FORBIDDEN | 403 | Authenticated but role insufficient for the operation. Also returned when an admin disables impersonation (ORG_INACTIVE-like 7-day deletion grace returns this). | Check the caller’s role + the target resource’s org. |
ACCOUNT_DISABLED | 403 | The user row has enabled=false. | Re-enable from /admin/users. |
ACCOUNT_LOCKED | 423 | Login lockout (5 consecutive failed logins, 15-min window). | Wait 15 min or have an admin reset. |
NOT_FOUND | 404 | Resource not in caller’s org (or never existed). v1 returns 404 (not 403) for cross-org access to avoid leaking existence. | Confirm ID + org ownership. |
CONFLICT | 409 | State conflict (duplicate email on create, etc.). | Re-fetch the resource and retry. |
GONE | 410 | Pairing code or shared-clip token expired. | Mint a new pairing code or share token. |
LIMIT_EXCEEDED | 422 | Server-side limit hit (max sessions per user, max alert-rule recipients, etc.). | Reduce the input or raise the org-level limit. |
UNPROCESSABLE | 422 | Semantic validation failure (well-formed but business-rule-violating). | Read message. |
REQUEST_TOO_LARGE | 413 | Payload exceeds limit (e.g. CSV export filter spans > EVENT_EXPORT_MAX_ROWS). | Narrow the filter or paginate. |
RATE_LIMITED | 429 | SA caller exceeded the 600/min bucket (or per-camera 1/5s trigger limit). | Back off; see Rate limit handling. Note: no Retry-After header is set in v1 — back off with exponential jitter. |
5xx — server errors
| Code | HTTP | Meaning | How to fix |
|---|---|---|---|
INTERNAL_ERROR | 500 | Unhandled server-side error. | Retry with exponential backoff. If persistent, capture the response + your request URL/time and forward to support. |
BAD_GATEWAY | 502 | Webhook delivery sub-status — outbound delivery target returned non-2xx or timed out. Not relevant to inbound API calls. | See Webhook delivery failures. |
AI_PROVIDER_UNAVAILABLE | 503 | The AI runtime / Gemini judge service is temporarily unreachable. | Retry; check the AI runtime health card at /admin/ai. |
Where the codes are wired
The full source-of-truth mapping is in apierror/errors.go (search the repo for errMap). If a code appears in a response that isn’t in this table, file a docs bug.
See also
- Request tracing — correlate failures with server logs
- Rate limit handling — how to interpret
RATE_LIMITED - API reference — full OpenAPI spec