> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ndi.nace.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors & limits

> The error envelope, HTTP status codes, and rate limits

## The error envelope

Every non-2xx response uses one JSON envelope:

```json theme={"dark"}
{
  "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`.            |

<Note>
  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.
</Note>

## 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:

```json theme={"dark"}
{
  "job_id": "…",
  "status": "failed",
  "error": { "code": "cancelled", "message": "cancelled by client request" }
}
```

Notable job error codes: `cancelled` (you cancelled it — see [Jobs](/concepts/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.
