Skip to main content

Canonical error codes

Machine-readable, stable, string-valued. Use these exact codes in the error.code field of API error responses (see docs/05-api-design.md + rule 30-api.mdc).

Never invent new codes in a PR without adding them here.

Format

{domain}.{kind} — two dot-separated segments, snake_case.

Validation (422)

CodeWhen
validation.field_requiredRequired field missing
validation.field_invalidField type/format invalid
validation.field_out_of_rangeNumeric/length out of allowed range
validation.conflictBusiness rule conflict (non-concurrency)
validation.unknown_fieldField not in schema

Auth (401 / 403)

CodeHTTPWhen
auth.invalid_token401JWT missing, malformed, expired, or bad signature
auth.unauthenticated401No credentials supplied
auth.mfa_required401Step-up MFA needed
auth.email_unverified403Email verification required
authz.forbidden403Authenticated but not permitted
authz.insufficient_scope403JWT scope missing required grant
authz.tenant_not_a_member403X-Tenant-Id not in user's tids

Resource (404 / 409 / 410 / 423)

CodeHTTPWhen
resource.not_found404Not found (or RLS-hidden)
resource.gone410Previously existed, now erased
resource.conflict409Create conflict (e.g., unique constraint)
resource.locked423Locked by another process

Precondition (412 / 428)

CodeHTTPWhen
precondition.failed412If-Match version mismatch
precondition.required428If-Match missing on optimistic-concurrency write

Idempotency (428 / 409)

CodeHTTPWhen
idempotency.key_missing428Idempotency-Key missing on write
idempotency.key_conflict409Same key, different body

Rate limiting (429)

CodeHTTPWhen
rate.limited429Request rate exceeded; includes Retry-After

Sync (409 / 410)

CodeHTTPWhen
sync.conflict.detected409Offline mutation conflicts with server state
sync.mutation.rejected409Mutation rejected (invariant violation)
sync.cursor.out_of_range410Sync cursor too old (full rebase required)

AI (various)

CodeHTTPWhen
ai.refused.safety422Pre-call or post-call safety blocked
ai.refused.budget429Tenant/purpose budget exceeded
ai.refused.provider502Upstream provider failure
ai.refused.policy403Feature flag off or tenant not opted in
ai.invalid_output502Structured output failed schema + repair

Upstream / service (5xx)

CodeHTTPWhen
internal.unhandled500Uncaught error (log + alert)
upstream.unavailable502Dependent service down
service.unavailable503This service shedding load / maintenance
upstream.timeout504Dependent call timed out

Error response template

{
"error": {
"type": "https://errors.ghasi.io/{domain}/{kind}",
"code": "{domain}.{kind}",
"title": "<short human summary>",
"status": <http status>,
"detail": "<specific explanation, safe to show to user>",
"instance": "<request path>",
"errors": [ { "field": "<optional field>", "code": "<optional sub-code>" } ],
"traceId": "<W3C trace id>",
"requestId": "<ULID>",
"retriable": <bool>,
"retryAfter": <seconds or null>,
"docUrl": "https://docs.ghasi.io/errors/{domain}/{kind}"
}
}

Rules

  • Never reuse a code across domains.
  • Never change the HTTP status for an existing code.
  • Never include stack traces or internal pointers in detail.
  • Always set traceId + requestId.
  • Always set retriable + retryAfter where a retry is sensible.
  • Never expose whether a resource exists across tenants (return resource.not_found, not a 403, for cross-tenant access attempts).