The error envelope
Every non-2xx response uses one JSON envelope:
{
"error": {
"code": "validation_error",
"message": "request body failed validation",
"request_id": "8b1f3c2ad94e4f0a"
}
}
| Field | Meaning |
|---|
error.code | Stable, machine-readable code — branch on this, not on message. |
error.message | Human-readable detail. Wording may change; don’t parse it. |
error.request_id | Unique 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
| Status | Code | When |
|---|
400 | bad_request | Malformed request. |
401 | unauthorized | Missing, invalid, or revoked API key. |
402 | quota_exceeded | Your included credits are used up — contact us to increase your allowance. |
403 | forbidden | The referenced storage location isn’t yours to use. |
404 | not_found | Job, 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. |
410 | — | Upload handle expired. Re-upload the file. |
413 | payload_too_large | Upload exceeds 200 MB. |
422 | validation_error | Body failed validation: unknown fields, bad URLs, unsupported file extension, missing file_name, … |
429 | too_many_requests | A rate limit was hit — see below. |
500 | internal_error | Our bug. Retry with the same idempotency_key; report the request_id if it persists. |
503 | service_unavailable | Temporary — 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.
| Limit | Default | On breach |
|---|
| Requests per minute | 60 | 429 with a Retry-After header |
| Concurrent jobs (pending + running) | 10 | 429 — wait for jobs to finish |
| Concurrent sync waits | 4 | not an error — the sync call degrades to 202 and you poll |
| Included credits | 50,000 | 402 — 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:
| Action | Credits |
|---|
| Parse — document (PDF, Word, image, …) | 1 per page |
| Parse — spreadsheet | 1 per 500 cells, per sheet, rounded up |
| Categorize | 2 per page (spreadsheets: 2 per sheet) |
| Ground | 5 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.