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"
}| Field | Always present | Meaning |
|---|---|---|
| statusCode | Yes | The HTTP status, repeated in the body. |
| code | Yes | UPPER_SNAKE error code (table below). |
| reason | No | Stable lowercase refusal token. Branch on it when present. |
| error | Yes | Short status text, e.g. "Forbidden". |
| message | Yes | Human-readable explanation. Do not parse it. |
| retryable | Yes | Whether retrying the same request can succeed. |
| details | No | Structured extras, e.g. required_scopes, violations, limit, used, reset_at. |
| request_id | Yes | Identifier of this request. Include it in support requests. |
| timestamp, path | Yes | When 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:
| reason | Status | Meaning |
|---|---|---|
| insufficient_grant | 403 | No approved grant covers the resource, or the route is unreachable by an app service key. |
| install_pending_approval | 403 | A newer app version is awaiting owner approval. |
| install_not_ready | 403 / 409 | The app install is not in force, is parked, or has no OAuth client. |
| endpoint_not_confirmed | 400 | The subscription endpoint has not been confirmed yet. |
| slot_unbound | 400 / 409 | A required slot is unbound. |
| slot_unknown | 400 | The alias names no declared slot. |
| hosted_manifest_read_only | 409 | The manifest is hosted by the vendor: edit it there, then refresh. |
| hosted_surface_fixed | 422 | A hosted app cannot set surface_url; drop the field. |
| manifest_fetch_failed | 422 | The vendor manifest URL could not be read; see details.violations. |
| schema_validation_failed | 422 | The manifest is invalid; see details.violations. |
| refresh_failed | 422 | Refreshing the hosted manifest failed; see details.changed and details.error. |
| insufficient_scope | 403 | The key or token lacks a scope; required_scopes lists what is missing. |
| unauthenticated | 403 | No principal could be resolved for the request. |
| workspace_required | 403 | A master-view key cannot call this route; use a workspace key. |
| insufficient_tier | 403 | Your role ranks below what the route requires; required states the minimum. |
| owner_required | 403 | A human workspace owner must perform this action. |
| insufficient_app_grant | 403 | App grants exist but at too low a level. |
| decide_grant_required | 403 | An 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:
| Code | Meaning |
|---|---|
| VALIDATION_ERROR | The request is malformed or a parameter is invalid. |
| AUTH_REQUIRED | Missing or invalid API key or token. |
| TOKEN_EXPIRED | The OAuth access token has expired; refresh it. |
| INSUFFICIENT_PERMISSIONS | Authenticated, but not allowed; check reason. |
| RESOURCE_NOT_FOUND | The resource does not exist in this workspace. |
| DUPLICATE_RESOURCE | A resource with the same identity already exists. |
| QUOTA_EXCEEDED | A plan quota has been reached. |
| INSUFFICIENT_CREDITS | The credit balance cannot cover the request. |
| FILE_TOO_LARGE | The upload exceeds your plan's file size limit (413). |
| OCR_FAILED | Text could not be read from the document. |
| EXTRACTION_FAILED | The document could not be extracted. |
| EXTRACTION_TIMEOUT | Extraction did not finish in time. |
| LLM_RATE_LIMITED | An upstream model provider is rate limiting; retry later. |
| LLM_TIMEOUT | An upstream model call timed out. |
| LLM_UNAVAILABLE | No upstream model is currently reachable. |
| INTERNAL_ERROR | Unexpected 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.