Skip to main content

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 ; in message.
  • 401 AUTH_REQUIRED — Missing, malformed, invalid, revoked, or expired credentials. The message distinguishes the cases (e.g. "Invalid or revoked API key."). An expired key surfaces here, not as TOKEN_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_scopes lists what the route needs; details.key_scopes lists 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 until details.reset_at (midnight UTC) before retrying.
  • 429 INGEST_OVERLOADED — The /v1/run ingest path is saturated on this replica. Retryable; the response carries a Retry-After header (seconds).
  • 429 PIPELINE_QUEUE_OVERLOADED — The pipeline queue has insufficient headroom for another Spec run. Retry after the Retry-After interval.
  • 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:

  1. 1st retry — wait 1 second
  2. 2nd retry — wait 2 seconds
  3. 3rd retry — wait 4 seconds
  4. 4th retry — wait 8 seconds
  5. 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.

Not every 429 waits until midnight: 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.

Frequently asked questions

What error codes does the Talonic API return?+
A typed code in the code field, including VALIDATION_ERROR (400), AUTH_REQUIRED (401), INSUFFICIENT_CREDITS and QUOTA_EXCEEDED (402), INSUFFICIENT_PERMISSIONS (403), RESOURCE_NOT_FOUND (404), FILE_TOO_LARGE (413), EXTRACTION_FAILED (422), plus retryable overload codes like INGEST_OVERLOADED (429) and INTERNAL_ERROR (500).
Which Talonic API errors should I retry?+
Retry errors with retryable: true — the 429 overload codes and 500-class timeouts and provider outages — using exponential backoff, honoring Retry-After when present. Also retry the daily-limit 429 (error: rate_limit_exceeded) after details.reset_at, even though its retryable flag is false. Never retry 400, 401, 402, 403, 404, 409, 413, or 422 without fixing the request first.
What is the difference between QUOTA_EXCEEDED and INSUFFICIENT_CREDITS?+
Both are 402s. INSUFFICIENT_CREDITS means your credit balance is spent — top up or wait for the Free-plan monthly grant. QUOTA_EXCEEDED means the workspace agent (POST /v1/ask) hit its monthly usage budget, which resets at the start of the next month or can be raised by an administrator. Neither is the daily request limit, which returns 429 with error: rate_limit_exceeded.
Does the Talonic API send a Retry-After header?+
Only on the transient overload 429s from POST /v1/run (INGEST_OVERLOADED and PIPELINE_QUEUE_OVERLOADED), which suggest a ~30 second wait. The daily rate-limit 429 carries no Retry-After; use the X-RateLimit-Reset header or details.reset_at from the body instead.