> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gavai.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> Error envelope, status codes, retry semantics.

export const Def = ({term, type, defaultValue, required, children}) => <>
    <dt className="gv-def-list__term">
      <span className="gv-def-list__name">{term}</span>
      {type && <span className="gv-def-list__type">{type}</span>}
      {required && <span className="gv-def-list__required">required</span>}
      {defaultValue !== undefined && <span className="gv-def-list__default">
          default <code>{defaultValue}</code>
        </span>}
    </dt>
    <dd className="gv-def-list__description">{children}</dd>
  </>;

export const DefList = ({children}) => <dl className="gv-def-list">{children}</dl>;

export const ErrorTable = ({errors}) => <div className="gv-error-table" role="table" aria-label="Errors and fixes">
    <div className="gv-error-table__head" role="row">
      <span className="gv-error-table__col-head" role="columnheader">Error</span>
      <span className="gv-error-table__col-head" role="columnheader">Cause</span>
      <span className="gv-error-table__col-head" role="columnheader">Fix</span>
    </div>
    {errors.map((err, i) => <div className="gv-error-table__row" role="row" key={i}>
        <code className="gv-error-table__code" role="cell">{err.code}</code>
        <div className="gv-error-table__cause" role="cell">{err.cause}</div>
        <div className="gv-error-table__fix" role="cell">{err.fix}</div>
      </div>)}
  </div>;

export const Eyebrow = ({label, tone = "default"}) => <div className={`gv-eyebrow gv-eyebrow--${tone}`}>
    <span className="gv-eyebrow__bar" aria-hidden="true" />
    <span className="gv-eyebrow__label">{label}</span>
  </div>;

<Eyebrow label="ERRORS" />

Every error from `https://api.gavai.app/v1` returns the same JSON envelope and a matching HTTP status code. The status tells you the problem class; the `error.code` tells you the specific reason. By the end of this page you will know which codes are safe to retry, what `details` carries for validation failures, and how to read scope errors.

## The envelope

```json theme={null}
{
  "error": {
    "code":    "insufficient_scope",
    "message": "Token lacks pages:write",
    "details": {
      "required_scope": "pages:write"
    }
  }
}
```

<DefList>
  <Def term="error.code" type="string" required>Always present. Machine-readable reason, e.g. `validation_failed`, `insufficient_scope`.</Def>
  <Def term="error.message" type="string" required>Always present. One-line human-readable summary.</Def>
  <Def term="error.details" type="object">Present when the error type benefits from structure — missing scope name, retry-after seconds, etc.</Def>
</DefList>

Validation errors (`400`) extend the envelope with a `fields` array, one entry per offending field:

```json theme={null}
{
  "error": {
    "code":    "validation_failed",
    "message": "Request body failed validation",
    "fields": [
      { "path": "name",  "message": "must not be empty" },
      { "path": "count", "message": "must be greater than or equal to 0" }
    ]
  }
}
```

## Status codes

<ErrorTable
  errors={[
{ code: "400", cause: "Validation failed — request body or query did not pass schema validation.", fix: "Not retry-safe. Fix the request first." },
{ code: "401", cause: "Authentication failed — token missing, malformed, or expired.", fix: "Not retry-safe. Fix or refresh the token first." },
{ code: "403", cause: "Insufficient scope or workspace access — token is valid but lacks the permission.", fix: "Not retry-safe. Mint a token with the correct scope." },
{ code: "404", cause: "Resource not found, or not visible to this token.", fix: "Not retry-safe." },
{ code: "409", cause: "Conflict — the operation conflicts with existing state (e.g. duplicate slug).", fix: "Not retry-safe. Resolve the conflict first." },
{ code: "429", cause: "Rate limit reached — too many requests from this token.", fix: "Retry-safe. Wait the `Retry-After` delay." },
{ code: "5xx", cause: "Server error on our side.", fix: "Retry-safe. Exponential backoff with jitter." },
]}
/>

## Common error codes

<ErrorTable
  errors={[
{ code: "token_expired", cause: "Access token has passed its expiry.", fix: "Refresh via `POST /v1/oauth/refresh` and retry." },
{ code: "insufficient_scope", cause: "Token is valid but missing the required scope.", fix: "Mint a token that holds the scope named in `details.required_scope`." },
{ code: "rate_limited", cause: "Per-token rate limit reached.", fix: "Wait the `Retry-After` header value (seconds) before retrying." },
{ code: "not_found", cause: "Resource does not exist or is not visible to this token.", fix: "Verify the id and that the token can see the workspace." },
{ code: "validation_failed", cause: "Request body or query failed schema validation.", fix: "Read `fields` for each problem and correct the request." },
{ code: "conflict", cause: "Operation cannot proceed because of existing state — for example, a slug already in use.", fix: "Resolve the underlying conflict, then retry with a corrected request." },
]}
/>

## Inspecting `details` by error class

The `details` object varies by `error.code`. The shapes worth knowing because handlers branch on them:

### Validation failures

`400 validation_failed` carries `fields[]` — one entry per offending field. Each entry has a JSON-pointer-style `path` and a `reason` that is one of a small enumerated set. Branch on `reason` if your client needs to render structured form errors:

```json theme={null}
{
  "error": {
    "code": "validation_failed",
    "fields": [
      { "path": "/email",      "reason": "format_mismatch", "message": "must be a valid email address" },
      { "path": "/name",       "reason": "too_short",       "message": "must be at least 2 characters" },
      { "path": "/permissions","reason": "unknown_value",   "message": "unknown scope: pages:nuke" }
    ]
  }
}
```

The common reasons are `missing`, `too_short`, `too_long`, `format_mismatch`, `out_of_range`, `unknown_value`, and `unknown_field`. Treat anything you don't recognize as a generic validation error.

### Conflict kinds

`409 conflict` carries `details.kind` so you can tell *what kind* of conflict happened. Each kind has a different recovery path:

<ErrorTable
  errors={[
{ code: "slug_in_use", cause: "A workspace, page, or domain with this slug already exists.", fix: "Pick a different slug, or update the existing resource if that's what you meant." },
{ code: "version_mismatch", cause: "The `If-Match` revision on a PATCH no longer matches the server-side version.", fix: "Re-read the resource, rebase your patch, and retry. The server returns the current version in `details.current_version`." },
{ code: "already_published", cause: "A deployment was promoted while a build was still in flight.", fix: "Wait for the in-flight build to settle, then promote the version you intended." },
{ code: "name_taken", cause: "A unique name (token name, domain hostname, plan id) is already used inside this workspace.", fix: "Pick a different name; uniqueness is per workspace, not global." },
{ code: "tombstoned", cause: "The resource was deleted recently and its slug is held in a grace window to prevent confusion.", fix: "Wait for the grace window to elapse, or pick a different slug." },
]}
/>

### Auth hints

`401 unauthorized` and `403 insufficient_scope` carry a `details.hint` that names the specific recovery action. The hint is enumerated so a client can branch on it without parsing the message:

<DefList>
  <Def term="hint = token_expired">Refresh via `POST /v1/oauth/refresh` and retry. The `Retry-After` header on a 401 with this hint is always 0.</Def>
  <Def term="hint = token_revoked">The token is permanently dead. Re-authenticate from scratch — refresh will not work.</Def>
  <Def term="hint = workspace_locked">The workspace is in a billing or compliance hold. Resolve the hold from the console before retrying.</Def>
  <Def term="hint = mfa_required">The operation requires an MFA challenge that has not been completed. Step the user through MFA and retry.</Def>
  <Def term="hint = missing_scope">The token is valid but lacks the scope named in `details.required_scope`. Mint a token with that scope.</Def>
</DefList>

### Upstream failures

When a capability dispatch fails because the downstream provider is unavailable — Stripe is rate-limiting, the email gateway returned 5xx, the database connection pool is saturated — the error envelope uses `code: "upstream_unavailable"` with `details.upstream` naming the failing service and `details.retryable` set to `true` when the dispatcher believes a retry will succeed:

```json theme={null}
{
  "error": {
    "code": "upstream_unavailable",
    "message": "The payments provider is temporarily unreachable. Try again in a few seconds.",
    "details": {
      "upstream": "payments",
      "retryable": true,
      "request_id": "req_01H..."
    }
  }
}
```

`upstream` is enumerated: `payments`, `email`, `storage`, `database`, `identity`, `dns`. Use it to surface a targeted message to your user rather than a generic "something went wrong."

### Policy denials

A request that hit a policy rule rather than a scope check returns `403 policy_denied` with a structured diagnostic naming the rule that fired. The diagnostic is intentionally minimal — it tells you *that* a rule blocked the request without revealing the rule's predicate:

```json theme={null}
{
  "error": {
    "code": "policy_denied",
    "message": "Policy did not permit this action.",
    "details": {
      "rule_id": "rule_workspace_quota_exceeded",
      "resource": "pages",
      "action": "publish"
    }
  }
}
```

`rule_id` is stable per rule; the console exposes a human-readable description for each. Display it to operators rather than end users.

## Retrying safely

Never retry a 4xx without changing the request — the outcome will not change. For 5xx and 429:

* Respect `Retry-After` on 429 before attempting again
* Apply exponential backoff with jitter on 5xx — start at 1 second, double each attempt, cap at a sensible ceiling
* Attach an `Idempotency-Key` header on writes so retries do not produce duplicate side effects (see [Idempotency](/api-reference/idempotency))

## Next

<Card title="Idempotency" icon="rotate" href="/api-reference/idempotency">
  How `Idempotency-Key` makes write retries safe across a 24-hour window.
</Card>
