Error Codes
Complete Talonic API error code reference: every machine-readable code with its HTTP status, retryable classification, and the recommended handling for each.
The Talonic API returns a machine-readable error code in the code field of every [error envelope](error-format). Codes fall into two groups: permanent errors (fix the request before retrying) and retryable errors (retry the same request with exponential backoff). The retryable boolean tells you which group you are in for most responses; the daily rate-limit 429 is the one exception, covered below.
Permanent errors (do not retry)
- 400
VALIDATION_ERROR— The request body is malformed, missing required fields, or contains an invalid schema definition. Multiple field failures are joined with;inmessage. - 401
AUTH_REQUIRED— Missing, malformed, invalid, revoked, or expired credentials. Themessagedistinguishes the cases (e.g. "Invalid or revoked API key."). An expired key surfaces here, not asTOKEN_EXPIRED. - 402
INSUFFICIENT_CREDITS— Your organization has no remaining credits. Top up from Settings → Billing, or wait for the next Free-plan monthly grant. - 402
QUOTA_EXCEEDED— The workspace agent (POST /v1/ask) has reached its monthly usage budget. It resets at the start of the next month, or an administrator can raise the limit. - 403
INSUFFICIENT_PERMISSIONS— The API key does not have the required scope for this operation.details.required_scopeslists what the route needs;details.key_scopeslists what your key carries. - 404
RESOURCE_NOT_FOUND— The requested resource does not exist (or belongs to another workspace). Talonic never distinguishes the two, so a 404 on a known id usually means the wrong workspace key. - 409
DUPLICATE_RESOURCE/RESOURCE_CONFLICT— A resource with this identifier already exists, or the operation conflicts with in-flight state (e.g. starting a run that is already running). - 413
FILE_TOO_LARGE— The uploaded file exceeds your API tier's upload limit (up to 500 MB on Enterprise). Rejected before any processing, so no credits are consumed. See [Rate Limits](rate-limits). - 413
PIPELINE_RUN_TOO_LARGE— A Spec run exceeds the per-run document ceiling. Split the submission into smaller batches. - 422
EXTRACTION_FAILED— The document could not be processed. Try a different format or check the file.
Retryable errors (retry with backoff)
- 429 daily rate limit (
error: "rate_limit_exceeded") — The daily request count for the namespace is exhausted. Wait untildetails.reset_at(midnight UTC) before retrying. - 429
INGEST_OVERLOADED— The/v1/runingest path is saturated on this replica. Retryable; the response carries aRetry-Afterheader (seconds). - 429
PIPELINE_QUEUE_OVERLOADED— The pipeline queue has insufficient headroom for another Spec run. Retry after theRetry-Afterinterval. - 429
LLM_RATE_LIMITED— Upstream AI provider rate limit. Retry after 30–60 seconds. - 500
LLM_TIMEOUT— Upstream AI provider timed out. Retryable with backoff. - 500
LLM_UNAVAILABLE— Upstream AI provider temporarily unavailable. - 500
OCR_FAILED— Document OCR failed. May succeed on retry. - 500
EXTRACTION_TIMEOUT— Extraction exceeded the time limit. Retryable for smaller documents. - 500
DATASPACE_RUN_FAILED— A platform Job (Structuring Run) failed server-side. Retryable with backoff. - 500
INTERNAL_ERROR— An unexpected server error occurred. Retryable with backoff.
The daily rate-limit 429 predates the typed code system: its body sets error: "rate_limit_exceeded" and details with limit, used, and reset_at, but its code field is not meaningful (it reads INTERNAL_ERROR) and retryable is false even though the request is perfectly safe to repeat after the reset. Identify it by the HTTP status plus the error label, never by code:
Daily rate-limit response (429)
{
"statusCode": 429,
"code": "INTERNAL_ERROR",
"error": "rate_limit_exceeded",
"message": "Daily extract request limit (1000) reached. Resets at midnight UTC.",
"retryable": false,
"details": {
"limit": 1000,
"used": 1000,
"reset_at": "2026-08-30T00:00:00.000Z"
},
"request_id": "req_9b1c22e04a5d47aa",
"timestamp": "2026-08-29T14:02:11.481Z",
"path": "/v1/extract"
}In addition to these canonical codes, input validation on POST /v1/extract returns lowercase error keys specific to that endpoint: missing_document, ambiguous_document, and invalid_options. They are documented in the [extract endpoint errors table](post-extract) and are always 400 and never retryable. TOKEN_EXPIRED remains reserved in the contract but does not surface on /v1 — an expired API key or OAuth token comes back as 401 AUTH_REQUIRED.
Recommended backoff strategy
For retryable errors, use exponential backoff with jitter, capped at five attempts:
- 1st retry — wait 1 second
- 2nd retry — wait 2 seconds
- 3rd retry — wait 4 seconds
- 4th retry — wait 8 seconds
- 5th retry (max) — wait 16 seconds
Error-aware retry handler
async function callWithRetry(fn: () => Promise<Response>, maxAttempts = 5) {
for (let attempt = 1; ; attempt++) {
const res = await fn();
if (res.ok) return res.json();
const body = await res.json();
const dailyLimit = res.status === 429 && body.error === 'rate_limit_exceeded';
const retryAfterHeader = res.headers.get('Retry-After');
if (attempt >= maxAttempts || (!body.retryable && !dailyLimit)) {
throw new Error(`${body.code}: ${body.message} (${body.request_id})`);
}
const waitMs = dailyLimit
? new Date(body.details.reset_at).getTime() - Date.now()
: retryAfterHeader
? Number(retryAfterHeader) * 1000
: Math.min(16000, 1000 * 2 ** (attempt - 1)) * (0.8 + Math.random() * 0.4);
await new Promise((r) => setTimeout(r, waitMs));
}
}Add random jitter (±20%) so many workers hitting the same limit do not retry in lockstep. When a Retry-After header is present — the /v1/run overload codes INGEST_OVERLOADED and PIPELINE_QUEUE_OVERLOADED send one — honor it instead of your own schedule. For the daily rate limit, sleep until details.reset_at (or the X-RateLimit-Reset header, which carries the same timestamp). Pair retries of POST requests with an [idempotency key](idempotency) so a retry can never create duplicate work.
INGEST_OVERLOADED and PIPELINE_QUEUE_OVERLOADED are transient load-shedding responses with a Retry-After of ~30 seconds, while rate_limit_exceeded binds until the daily window resets. Branch on the code/error fields before choosing a wait.