Skip to main content
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

{
  "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"
    }
  }
}
FieldTypeMeaning
error.codestringStable machine-readable code. Prefixed by category (auth., request., rate_limit., session., gate., etc.). Branch on this.
error.messagestringHuman-readable explanation. Safe to log; safe to surface to support or internal tooling. Do not pattern-match against it — it may change.
error.statusnumberHTTP status code. Same as the response status.
error.retryablebooleanWhether retrying the same request can reasonably succeed. true on 429 and 5xx; false on 4xx auth and validation failures.
error.request_idstringUnique identifier for this request. Include it verbatim in any support correspondence.
error.docs_urlstring, optionalWhen present, a deep-link to the most relevant docs section for this failure class.
error.detailsobject, optionalCategory-specific data. See Details field below.

HTTP status codes

StatusWhen
200Successful JSON response.
201Resource created (management endpoints).
204Successful, no response body (some DELETE operations).
400Malformed JSON, malformed required headers, or protocol-level input that can’t be parsed.
401Missing Authorization header.
403API key is present but invalid, revoked, or not authorized for this endpoint / resource.
404Resource not found, or not visible to the authenticated key.
409Conflict — commonly a duplicate resource (e.g. creating an organization slug that already exists).
422Validation failure — the request is well-formed but semantically invalid (e.g. required fields missing, values out of range).
429Rate limit exceeded. Response includes Retry-After header.
5xxServer 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.
PrefixMeaningCodes
auth.*Authentication or authorization failureauth.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 validationrequest.validation_failed, request.not_found, request.invalid_json, request.invalid_content_type, request.payload_too_large, request.conflict
rate_limit.*Rate limit exceededrate_limit.exceeded
billing.*Plan or quota limit reachedbilling.plan_limit_reached
session.*Collect/durable session statesession.invalid_or_expired, session.invalid_state, session.nonce_consumed, session.sequence_mismatch
transport.*The client bundle or wire format is out of datetransport.upgrade_required, transport.session_reset_required
gate.*Gate (agentic signup) errorsgate.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 retryinternal.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 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)

{
  "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_actionWhat to do
new_sessionThe session is unrecoverable. Start a fresh Foil session on the client.
reload_bundleThe 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.

Enumerated fields

When a field only accepts a fixed set of values, the rejected field’s expected lists them.
{
  "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-Remaining0 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 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.
Node.js
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:
SDKClass names
Node.jsFoilApiError, FoilTokenVerificationError, FoilConfigurationError
PythonFoilApiError, FoilTokenVerificationError, FoilConfigurationError
GoAPIError, TokenVerificationError, ConfigurationError
RubyFoil::Server::ApiError, Foil::Server::TokenVerificationError, Foil::Server::ConfigurationError
PHPFoil\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

Authentication

Key types, scopes, and lifecycle.

Pagination

Iterating through list endpoints.

API introduction

Surface-level summary of the API.

Troubleshooting

Operational guidance when things go wrong.