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

# Jobs, idempotency, and async semantics

> Understanding the unified job resource and replay-safe requests

All operations that take more than a moment return a `Job` resource. Every
job-creating route accepts `?wait_seconds=` to optionally block for a result.

***

## Job states

Every job flows through these states:

| State       | Meaning                                                             |
| ----------- | ------------------------------------------------------------------- |
| `queued`    | Waiting for a worker                                                |
| `running`   | Processing                                                          |
| `succeeded` | `result` field is populated and retained until `payload_expires_at` |
| `failed`    | `error` field is populated                                          |
| `cancelled` | Cancelled by caller or system                                       |

***

## Sync vs async via `wait_seconds`

The `?wait_seconds=` query parameter controls blocking on each job-creating route:

```bash theme={"dark"}
# Immediate 202 (default: wait_seconds=0)
curl -X POST "$NDI_BASE_URL/v1/parse" \
  -H "X-API-Key: $NDI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"source": {"type": "url", "url": "https://example.com/report.pdf", "file_name": "report.pdf"}}'
# → 202 with a queued Job

# Block up to 120 seconds for a result
curl -X POST "$NDI_BASE_URL/v1/parse?wait_seconds=120" \
  -H "X-API-Key: $NDI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"source": {"type": "url", "url": "https://example.com/report.pdf", "file_name": "report.pdf"}}'
# → 200 if job finished in time, 202 if it didn't
```

**Response codes:**

* **200** — Job reached a terminal state within `wait_seconds`; full result is inline.
* **202** — Timeout before completion; poll `GET /v1/jobs/{job_id}`.

`wait_seconds` max is 300. It is always a query parameter, never a JSON body field.

***

## Polling

```bash theme={"dark"}
# Poll until terminal
JOB_ID="550e8400-e29b-41d4-a716-446655440003"

until [[ $(curl -s "$NDI_BASE_URL/v1/jobs/$JOB_ID" -H "X-API-Key: $NDI_API_KEY" | jq -r '.status') =~ succeeded|failed|cancelled ]]; do
  sleep 3
done

curl -s "$NDI_BASE_URL/v1/jobs/$JOB_ID" -H "X-API-Key: $NDI_API_KEY"
```

**Succeeded:**

```json theme={"dark"}
{
  "job_id": "550e8400-e29b-41d4-a716-446655440003",
  "kind": "parse",
  "status": "succeeded",
  "result": {"result_type": "parse", "...": "..."},
  "units": 24,
  "created_at": "2026-08-09T12:00:00Z",
  "updated_at": "2026-08-09T12:01:23Z",
  "payload_expires_at": "2026-08-16T12:01:23Z"
}
```

**Failed:**

```json theme={"dark"}
{
  "job_id": "550e8400-e29b-41d4-a716-446655440003",
  "kind": "parse",
  "status": "failed",
  "error": {"code": "unsupported_file_type", "message": "...", "detail": null, "retryable": false, "request_id": "req-01j9..."},
  "units": 0,
  "created_at": "2026-08-09T12:00:00Z",
  "updated_at": "2026-08-09T12:00:08Z",
  "payload_expires_at": null
}
```

***

## Streaming job events

Subscribe to real-time status transitions instead of polling:

```bash theme={"dark"}
curl "$NDI_BASE_URL/v1/jobs/$JOB_ID/events" \
  -H "X-API-Key: $NDI_API_KEY" \
  -H "Accept: text/event-stream"
```

Returns `text/event-stream` (Server-Sent Events). Each event is a complete `Job`
JSON object. The stream closes when the job reaches a terminal state.

***

## Idempotency

Use the `Idempotency-Key` HTTP header to make job-creating requests replay-safe:

```bash theme={"dark"}
curl -X POST "$NDI_BASE_URL/v1/parse" \
  -H "X-API-Key: $NDI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: parse-annual-report-2025-v1" \
  -d '{"source": {"type": "url", "url": "https://example.com/report.pdf", "file_name": "report.pdf"}}'
```

**Behavior:**

* **First call** — Starts a new job.
* **Repeated calls with the same key** — Returns the original job without starting a new one.
* The key is scoped per API key.

`Idempotency-Key` is always an HTTP header, never a JSON body field.

**Use for:**

* Retrying after a network failure (the same job is returned)
* CI/CD pipelines (re-running a step is safe)

***

## Listing jobs

```bash theme={"dark"}
curl "$NDI_BASE_URL/v1/jobs" \
  -H "X-API-Key: $NDI_API_KEY"
```

Returns a keyset-paginated page (newest first):

```json theme={"dark"}
{
  "items": [...],
  "next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0wOC0wOVQxMjowMDowMFoiLCJqb2JfaWQiOiIuLi4ifQ==",
  "total_count": 142
}
```

Pass `?cursor=<next_cursor>` to retrieve the next page. Optional filters: `?status=succeeded`, `?kind=parse`.

***

## Cancellation

```bash theme={"dark"}
curl -X POST "$NDI_BASE_URL/v1/jobs/$JOB_ID/cancel" \
  -H "X-API-Key: $NDI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason": "No longer needed"}'
```

* **Queued or running** — Job is marked `cancelled`.
* **Already terminal** — Idempotent; returns the existing terminal job.
