Skip to main content

The error envelope

Every non-2xx response uses one JSON envelope:
{
  "error": {
    "code": "validation_error",
    "message": "request body failed validation",
    "request_id": "8b1f3c2ad94e4f0a"
  }
}
FieldMeaning
error.codeStable, machine-readable code — branch on this, not on message.
error.messageHuman-readable detail. Wording may change; don’t parse it.
error.request_idUnique id for this request. Include it when contacting support.
Validation failures (422) additionally carry an error.errors array with per-field details in FastAPI/Pydantic format. Every response — success or failure — also carries the request id in the X-Request-Id header. You may send your own X-Request-Id (alphanumeric, -, _; max 128 chars) and NDI will propagate it.

HTTP status codes

StatusCodeWhen
400bad_requestMalformed request.
401unauthorizedMissing, invalid, or revoked API key.
402quota_exceededYour included credits are used up — contact us to increase your allowance.
403forbiddenThe referenced storage location isn’t yours to use.
404not_foundJob, file handle, or pagination cursor doesn’t exist (or belongs to another tenant).
408— (sync_timeout in detail)Sync call exceeded timeout_seconds; the job keeps running — poll it.
410Upload handle expired. Re-upload the file.
413payload_too_largeUpload exceeds 200 MB.
422validation_errorBody failed validation: unknown fields, bad URLs, unsupported file extension, missing file_name, …
429too_many_requestsA rate limit was hit — see below.
500internal_errorOur bug. Retry with the same idempotency_key; report the request_id if it persists.
503service_unavailableTemporary — the job could not be enqueued. Safe to retry with the same idempotency_key.
Request bodies are strict: unknown fields are rejected with 422 rather than silently ignored. A typo in a field name fails loudly instead of dropping your option on the floor.

Job-level errors

An accepted job that later fails does not surface as an HTTP error — the job’s status becomes failed and its error field is populated:
{
  "job_id": "…",
  "status": "failed",
  "error": { "code": "cancelled", "message": "cancelled by client request" }
}
Notable job error codes: cancelled (you cancelled it — see Jobs).

Rate limits & quota

Four independent limits protect the service. Defaults below are per tenant and can be raised — talk to us.
LimitDefaultOn breach
Requests per minute60429 with a Retry-After header
Concurrent jobs (pending + running)10429 — wait for jobs to finish
Concurrent sync waits4not an error — the sync call degrades to 202 and you poll
Included credits50,000402 — contact us to increase your allowance

How credits are counted

Every job consumes credits, and the cost is always computable from your request before you send it:
ActionCredits
Parse — document (PDF, Word, image, …)1 per page
Parse — spreadsheet1 per 500 cells, per sheet, rounded up
Categorize2 per page (spreadsheets: 2 per sheet)
Ground5 per item requested
Credits are charged on what you send — pages and requested items — never on outputs, tokens, or detected document complexity. Consumption is recorded when a job finishes; failed jobs generally consume nothing. Two deliberate carve-outs:
  • Polling is free. GET /jobs and GET /jobs/{id} are never rate-limited, so poll without backoff anxiety.
  • Idempotent replays count toward RPM (they consume request capacity like any other call) but never toward the concurrency limit (no new job is created).

Retry guidance

  • 429 — respect Retry-After, then retry.
  • 402 — not retryable; your credit allowance needs a top-up.
  • 503 / network errors on a POST — retry with the same idempotency_key; you will either resume the original job or start it exactly once.
  • 408 / 202 on sync endpoints — not failures; switch to polling GET /jobs/{job_id}.
  • 4xx (other) — a bug in the request; fix it before retrying.