Skip to main content

Error Handling

Every Talonic API error uses the same JSON envelope: the HTTP status, a machine-readable code, an optional stable reason for refusals, a human-readable message, and a request_id for support.

Error response format

# Example error response
{
  "statusCode": 403,
  "code": "INSUFFICIENT_PERMISSIONS",
  "reason": "insufficient_scope",
  "error": "Forbidden",
  "message": "This route requires the 'records_write' scope.",
  "retryable": false,
  "details": { "required_scopes": ["records_write"] },
  "request_id": "b6f1c2e0-...",
  "timestamp": "2026-10-06T09:14:03.120Z",
  "path": "/v1/record-sets/rs_123/records"
}
FieldAlways presentMeaning
statusCodeYesThe HTTP status, repeated in the body.
codeYesUPPER_SNAKE error code (table below).
reasonNoStable lowercase refusal token. Branch on it when present.
errorYesShort status text, e.g. "Forbidden".
messageYesHuman-readable explanation. Do not parse it.
retryableYesWhether retrying the same request can succeed.
detailsNoStructured extras, e.g. required_scopes, violations, limit, used, reset_at.
request_idYesIdentifier of this request. Include it in support requests.
timestamp, pathYesWhen and where the error occurred.

Refusal reasons

When the API refuses a request for an authorization or configuration reason, it sets a top-level reason next to code. Reasons are stable tokens: branch on reason rather than on the status or message, because several reasons share one status (most are 403). The full list, from the OpenAPI spec:

reasonStatusMeaning
insufficient_grant403No approved grant covers the resource, or the route is unreachable by an app service key.
install_pending_approval403A newer app version is awaiting owner approval.
install_not_ready403 / 409The app install is not in force, is parked, or has no OAuth client.
endpoint_not_confirmed400The subscription endpoint has not been confirmed yet.
slot_unbound400 / 409A required slot is unbound.
slot_unknown400The alias names no declared slot.
hosted_manifest_read_only409The manifest is hosted by the vendor: edit it there, then refresh.
hosted_surface_fixed422A hosted app cannot set surface_url; drop the field.
manifest_fetch_failed422The vendor manifest URL could not be read; see details.violations.
schema_validation_failed422The manifest is invalid; see details.violations.
refresh_failed422Refreshing the hosted manifest failed; see details.changed and details.error.
insufficient_scope403The key or token lacks a scope; required_scopes lists what is missing.
unauthenticated403No principal could be resolved for the request.
workspace_required403A master-view key cannot call this route; use a workspace key.
insufficient_tier403Your role ranks below what the route requires; required states the minimum.
owner_required403A human workspace owner must perform this action.
insufficient_app_grant403App grants exist but at too low a level.
decide_grant_required403An explicit decide grant (and live workspace membership) is needed.

Error codes

The code field is always UPPER_SNAKE. When an endpoint does not set one explicitly, it is derived from the status: 400 → VALIDATION_ERROR, 401 → AUTH_REQUIRED, 402 → INSUFFICIENT_CREDITS, 403 → INSUFFICIENT_PERMISSIONS, 404 → RESOURCE_NOT_FOUND, anything else → INTERNAL_ERROR. The public codes:

CodeMeaning
VALIDATION_ERRORThe request is malformed or a parameter is invalid.
AUTH_REQUIREDMissing or invalid API key or token.
TOKEN_EXPIREDThe OAuth access token has expired; refresh it.
INSUFFICIENT_PERMISSIONSAuthenticated, but not allowed; check reason.
RESOURCE_NOT_FOUNDThe resource does not exist in this workspace.
DUPLICATE_RESOURCEA resource with the same identity already exists.
QUOTA_EXCEEDEDA plan quota has been reached.
INSUFFICIENT_CREDITSThe credit balance cannot cover the request.
FILE_TOO_LARGEThe upload exceeds your plan's file size limit (413).
OCR_FAILEDText could not be read from the document.
EXTRACTION_FAILEDThe document could not be extracted.
EXTRACTION_TIMEOUTExtraction did not finish in time.
LLM_RATE_LIMITEDAn upstream model provider is rate limiting; retry later.
LLM_TIMEOUTAn upstream model call timed out.
LLM_UNAVAILABLENo upstream model is currently reachable.
INTERNAL_ERRORUnexpected server failure.

Retry strategy

Use the retryable flag: retry only when it is true, with exponential backoff and jitter. A 429 means a daily request limit for your API tier was reached; details.reset_at says when it resets (midnight UTC). Never retry a refusal that carries a reason unchanged: fix the scope, grant, or role first. For POST requests that create work, send an Idempotency-Key so a retry cannot run the work twice. See the webhook retry policy for delivery-side retries.

Frequently asked questions

Which field should my integration branch on?+
Branch on reason when it is present: it is a stable lowercase token (for example insufficient_scope or decide_grant_required) that names exactly why a request was refused. Otherwise branch on code (for example VALIDATION_ERROR or FILE_TOO_LARGE). Never parse message, which is written for humans and may change.
How do I handle rate limit errors?+
A 429 means a daily request limit for your API tier was reached. The details object carries limit, used and reset_at; limits reset at midnight UTC. Wait until reset_at, or move to a higher tier.
Are error responses consistent across all endpoints?+
Yes. A single global handler shapes every error into the same JSON envelope with statusCode, code, error, message, retryable, request_id, timestamp and path, plus reason and details when they apply.
What should I send to support?+
The request_id from the error body. It identifies the exact request in Talonic logs.