Error Codes
:::info Source
Sourced from docs/standards/ERROR_CODES.md in the documentation repo.
:::
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)
| Code | When |
|---|---|
validation.field_required | Required field missing |
validation.field_invalid | Field type/format invalid |
validation.field_out_of_range | Numeric/length out of allowed range |
validation.conflict | Business rule conflict (non-concurrency) |
validation.unknown_field | Field not in schema |
Auth (401 / 403)
| Code | HTTP | When |
|---|---|---|
auth.invalid_token | 401 | JWT missing, malformed, expired, or bad signature |
auth.unauthenticated | 401 | No credentials supplied |
auth.mfa_required | 401 | Step-up MFA needed |
auth.email_unverified | 403 | Email verification required |
authz.forbidden | 403 | Authenticated but not permitted |
authz.insufficient_scope | 403 | JWT scope missing required grant |
authz.tenant_not_a_member | 403 | X-Tenant-Id not in user's tids |
Resource (404 / 409 / 410 / 423)
| Code | HTTP | When |
|---|---|---|
resource.not_found | 404 | Not found (or RLS-hidden) |
resource.gone | 410 | Previously existed, now erased |
resource.conflict | 409 | Create conflict (e.g., unique constraint) |
resource.locked | 423 | Locked by another process |
Precondition (412 / 428)
| Code | HTTP | When |
|---|---|---|
precondition.failed | 412 | If-Match version mismatch |
precondition.required | 428 | If-Match missing on optimistic-concurrency write |
Idempotency (428 / 409)
| Code | HTTP | When |
|---|---|---|
idempotency.key_missing | 428 | Idempotency-Key missing on write |
idempotency.key_conflict | 409 | Same key, different body |
Rate limiting (429)
| Code | HTTP | When |
|---|---|---|
rate.limited | 429 | Request rate exceeded; includes Retry-After |
Sync (409 / 410)
| Code | HTTP | When |
|---|---|---|
sync.conflict.detected | 409 | Offline mutation conflicts with server state |
sync.mutation.rejected | 409 | Mutation rejected (invariant violation) |
sync.cursor.out_of_range | 410 | Sync cursor too old (full rebase required) |
AI (various)
| Code | HTTP | When |
|---|---|---|
ai.refused.safety | 422 | Pre-call or post-call safety blocked |
ai.refused.budget | 429 | Tenant/purpose budget exceeded |
ai.refused.provider | 502 | Upstream provider failure |
ai.refused.policy | 403 | Feature flag off or tenant not opted in |
ai.invalid_output | 502 | Structured output failed schema + repair |
Upstream / service (5xx)
| Code | HTTP | When |
|---|---|---|
internal.unhandled | 500 | Uncaught error (log + alert) |
upstream.unavailable | 502 | Dependent service down |
service.unavailable | 503 | This service shedding load / maintenance |
upstream.timeout | 504 | Dependent 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+retryAfterwhere a retry is sensible. - Never expose whether a resource exists across tenants (return
resource.not_found, not a 403, for cross-tenant access attempts).