error.code as the stable, machine-readable field and error.message as human-facing guidance that may evolve.
Error envelope
| 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 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. |
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 |
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 structureddetails object with extra context.
Field errors (request.validation_failed)
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 rejectedexpected— an optional human-readable description of what’s validreceived— the value the server saw (redacted for sensitive fields)
Next action hints
Some errors include anext_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. |
error.retryable and the HTTP status — see Retry strategy.
Enumerated fields
When a field only accepts a fixed set of values, the rejected field’sexpected lists them.
Rate limiting
Rate-limited responses return429 Too Many Requests with:
Retry-Afterheader — seconds until the next acceptable retryX-RateLimit-Limit— the ceiling for this organization and key typeX-RateLimit-Remaining—0at the point of rejection
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.comabout a suspected security issue - Open a support ticket about an unexpected error
- Correlate a client-side report with server-side logs
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
| 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 |
status, code, request_id, field_errors, docs_url, and the raw response body.
Retry strategy
A minimal retry loop that handles the common cases:- Read
error.retryable. Iffalse, don’t retry — the request will fail the same way every time. - If the status is
429, wait forRetry-Afterseconds before retrying. - If the status is
5xx, retry with exponential backoff (e.g. 0.5s, 1s, 2s, 4s, capped at 30s) with jitter. - Cap total retries at 3–5 attempts before surfacing the error to the caller.
- Always log
error.request_idon the last attempt so support can correlate.
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.