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

# Errors

> Handle Foil API errors with the structured JSON envelope, stable error codes, HTTP status codes, retry semantics, and request IDs for support.

Every non-2xx response from the Foil API returns a structured JSON envelope. Treat `error.code` as the stable, machine-readable field and `error.message` as human-facing guidance that may evolve.

## Error envelope

```json theme={"dark"}
{
  "error": {
    "code": "auth.missing_api_key",
    "message": "Missing Authorization header. Send Authorization: Bearer <token> to authenticate this request.",
    "status": 401,
    "retryable": false,
    "request_id": "req_0123456789abcdef0123456789abcdef",
    "docs_url": "https://usefoil.com/docs/api-reference/authentication",
    "details": {
      "next_action": "retry"
    }
  }
}
```

| Field              | Type             | Meaning                                                                                                                                   |
| ------------------ | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `error.code`       | string           | Stable machine-readable code. Prefixed by category (`auth.`, `request.`, `rate_limit.`, `session.`, `gate.`, etc.). Branch on this.       |
| `error.message`    | string           | Human-readable explanation. Safe to log; safe to surface to support or internal tooling. Do not pattern-match against it — it may change. |
| `error.status`     | number           | HTTP status code. Same as the response status.                                                                                            |
| `error.retryable`  | boolean          | Whether retrying the same request can reasonably succeed. `true` on 429 and 5xx; `false` on 4xx auth and validation failures.             |
| `error.request_id` | string           | Unique identifier for this request. Include it verbatim in any support correspondence.                                                    |
| `error.docs_url`   | string, optional | When present, a deep-link to the most relevant docs section for this failure class.                                                       |
| `error.details`    | object, optional | Category-specific data. See [Details field](#details-field) below.                                                                        |

## HTTP status codes

| Status | When                                                                                                                          |
| ------ | ----------------------------------------------------------------------------------------------------------------------------- |
| `200`  | Successful JSON response.                                                                                                     |
| `201`  | Resource created (management endpoints).                                                                                      |
| `204`  | Successful, no response body (some DELETE operations).                                                                        |
| `400`  | Malformed JSON, malformed required headers, or protocol-level input that can't be parsed.                                     |
| `401`  | Missing `Authorization` header.                                                                                               |
| `403`  | API key is present but invalid, revoked, or not authorized for this endpoint / resource.                                      |
| `404`  | Resource not found, or not visible to the authenticated key.                                                                  |
| `409`  | Conflict — commonly a duplicate resource (e.g. creating an organization slug that already exists).                            |
| `422`  | Validation failure — the request is well-formed but semantically invalid (e.g. required fields missing, values out of range). |
| `429`  | Rate limit exceeded. Response includes `Retry-After` header.                                                                  |
| `5xx`  | Server error. Transient — safe to retry with exponential backoff.                                                             |

4xx responses indicate the caller needs to change something before retrying; 5xx responses indicate Foil's side needs to recover. The `error.retryable` field encodes this directly and is safe to key retry logic off.

## Error code taxonomy

Error codes are namespaced with a category prefix. The prefix tells you which layer rejected the request; the suffix tells you specifically what went wrong.

| Prefix         | Meaning                                                               | Codes                                                                                                                                                                                                                                                                                      |
| -------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `auth.*`       | Authentication or authorization failure                               | `auth.missing_api_key`, `auth.invalid_api_key`, `auth.missing_bearer_token`, `auth.invalid_bearer_token`, `auth.secret_key_required`, `auth.secret_key_not_allowed`, `auth.insufficient_scope`, `auth.origin_not_allowed`, `auth.organization_inactive`, `auth.organization_access_denied` |
| `request.*`    | The request was malformed, too large, not found, or failed validation | `request.validation_failed`, `request.not_found`, `request.invalid_json`, `request.invalid_content_type`, `request.payload_too_large`, `request.conflict`                                                                                                                                  |
| `rate_limit.*` | Rate limit exceeded                                                   | `rate_limit.exceeded`                                                                                                                                                                                                                                                                      |
| `billing.*`    | Plan or quota limit reached                                           | `billing.plan_limit_reached`                                                                                                                                                                                                                                                               |
| `session.*`    | Collect/durable session state                                         | `session.invalid_or_expired`, `session.invalid_state`, `session.nonce_consumed`, `session.sequence_mismatch`                                                                                                                                                                               |
| `transport.*`  | The client bundle or wire format is out of date                       | `transport.upgrade_required`, `transport.session_reset_required`                                                                                                                                                                                                                           |
| `gate.*`       | Gate (agentic signup) errors                                          | `gate.session_not_found`, `gate.session_expired`, `gate.session_already_resolved`, `gate.bot_detected`, `gate.validation_failed`, `gate.rate_limit`, `gate.delivery_consumed`                                                                                                              |
| `internal.*`   | Server-side failure; safe to retry                                    | `internal.unavailable`                                                                                                                                                                                                                                                                     |

Branch your error handling on the category prefix first, the specific code second. A generic "auth failure" path that handles any `auth.*` code is usually correct, with specific codes adding nuance only where your UX demands it (e.g. handling `auth.origin_not_allowed` by reminding the developer to add their domain to the key's allowed origins, or `auth.insufficient_scope` by pointing at the missing scope).

The browser SDK surfaces its own client-side codes — `config.*` and `runtime.*`, plus `transport.upgrade_required` — documented alongside the SDK. See [Browser SDK → Error codes](/browser-sdk#error-codes) for the full table, including retry semantics and the recommended fallback policy.

## Details field

Some error codes include a structured `details` object with extra context.

### Field errors (`request.validation_failed`)

```json theme={"dark"}
{
  "error": {
    "code": "request.validation_failed",
    "message": "One or more fields failed validation.",
    "status": 422,
    "retryable": false,
    "request_id": "req_...",
    "details": {
      "fields": [
        {
          "name": "webhook_endpoint_id",
          "issue": "not_found",
          "expected": "active webhook endpoint subscribed to gate.session.approved",
          "received": "we_missing"
        }
      ]
    }
  }
}
```

Each `fields[]` entry carries:

* `name` — dot-path to the offending field in the request body (or query)
* `issue` — a stable short code describing why the value was rejected
* `expected` — an optional human-readable description of what's valid
* `received` — the value the server saw (redacted for sensitive fields)

### Next action hints

Some errors include a `next_action` in `details` to make retry logic straightforward:

| `next_action`   | What to do                                                                            |
| --------------- | ------------------------------------------------------------------------------------- |
| `new_session`   | The session is unrecoverable. Start a fresh Foil session on the client.               |
| `reload_bundle` | The browser bundle is stale or unrecognized. Reload the page so a fresh `t.js` loads. |

For everything else, key your retry logic off `error.retryable` and the HTTP status — see [Retry strategy](#retry-strategy).

### Enumerated fields

When a field only accepts a fixed set of values, the rejected field's `expected` lists them.

```json theme={"dark"}
{
  "error": {
    "code": "request.validation_failed",
    "message": "One or more fields failed validation.",
    "status": 422,
    "details": {
      "fields": [
        {
          "name": "status",
          "issue": "invalid_value",
          "expected": "active, suspended, or deleted",
          "received": "archived"
        }
      ]
    }
  }
}
```

## Rate limiting

Rate-limited responses return `429 Too Many Requests` with:

* `Retry-After` header — seconds until the next acceptable retry
* `X-RateLimit-Limit` — the ceiling for this organization and key type
* `X-RateLimit-Remaining` — `0` at the point of rejection

Respect `Retry-After`. Don't retry inside the window — further requests count against the limit and typically extend the cooldown. For bulk workflows, the server SDKs include built-in retry-with-backoff that honors this header.

See [Authentication → Rate limits](/api-reference/authentication#rate-limits) for organization-level defaults.

## Using `request_id`

Every response (success and failure) carries a `request_id` in the `meta` block for successful responses, and in `error.request_id` for failures. Include it verbatim when you:

* Email `security@usefoil.com` about a suspected security issue
* Open a support ticket about an unexpected error
* Correlate a client-side report with server-side logs

The ID looks like `req_0123456789abcdef0123456789abcdef` (32 hex characters after the prefix). It's unique per request and never reused.

## Handling errors in the server SDKs

Each server SDK throws structured error types that mirror the envelope above. The Node SDK's types illustrate the pattern every SDK follows.

```javascript Node.js theme={"dark"}
const {
  FoilApiError,
  FoilTokenVerificationError,
  FoilConfigurationError,
  safeVerifyFoilToken,
} = require("@abxy/foil-server");

try {
  const session = await client.sessions.get(sessionId);
} catch (err) {
  if (err instanceof FoilApiError) {
    console.error("API error", {
      code: err.code,
      status: err.status,
      requestId: err.request_id,
      fields: err.field_errors,
      docsUrl: err.docs_url,
    });

    if (err.status >= 500 || err.status === 429) {
      // Retry with backoff — use err.body.error.retryable in edge cases
    }
  } else if (err instanceof FoilConfigurationError) {
    // Deployment misconfiguration (missing secret, bad fetch) — fail loud
    throw err;
  }
}

// Token verification has its own exception, since it's a local op
const result = safeVerifyFoilToken(sealedToken, process.env.FOIL_SECRET_KEY);
if (!result.ok) {
  // result.error is a FoilTokenVerificationError
  return reject("invalid_foil_token");
}
```

Error classes across the SDKs:

| SDK     | Class names                                                                                          |
| ------- | ---------------------------------------------------------------------------------------------------- |
| Node.js | `FoilApiError`, `FoilTokenVerificationError`, `FoilConfigurationError`                               |
| Python  | `FoilApiError`, `FoilTokenVerificationError`, `FoilConfigurationError`                               |
| Go      | `APIError`, `TokenVerificationError`, `ConfigurationError`                                           |
| Ruby    | `Foil::Server::ApiError`, `Foil::Server::TokenVerificationError`, `Foil::Server::ConfigurationError` |
| PHP     | `Foil\Server\ApiError`, `Foil\Server\TokenVerificationError`, `Foil\Server\ConfigurationError`       |

All expose equivalent fields — `status`, `code`, `request_id`, `field_errors`, `docs_url`, and the raw response body.

## Retry strategy

A minimal retry loop that handles the common cases:

1. Read `error.retryable`. If `false`, don't retry — the request will fail the same way every time.
2. If the status is `429`, wait for `Retry-After` seconds before retrying.
3. If the status is `5xx`, retry with exponential backoff (e.g. 0.5s, 1s, 2s, 4s, capped at 30s) with jitter.
4. Cap total retries at 3–5 attempts before surfacing the error to the caller.
5. Always log `error.request_id` on the last attempt so support can correlate.

The server SDKs ship with this logic built in — you generally only need to write your own for custom HTTP clients.

## What's next

<CardGroup cols={2}>
  <Card title="Authentication" icon="lock-keyhole" href="/api-reference/authentication">
    Key types, scopes, and lifecycle.
  </Card>

  <Card title="Pagination" icon="list-ordered" href="/api-reference/pagination">
    Iterating through list endpoints.
  </Card>

  <Card title="API introduction" icon="book-open" href="/api-reference/introduction">
    Surface-level summary of the API.
  </Card>

  <Card title="Troubleshooting" icon="life-buoy" href="/resources/troubleshooting">
    Operational guidance when things go wrong.
  </Card>
</CardGroup>
