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

> Job lifecycle and Ground crop delivery — 5 endpoints

The unified job resource covers all async operations: document operations, ingestions,
knowledge-graph builds, intelligent search, file operations, and reconciliation.
Every job-creating method returns a `Job` and accepts `?wait_seconds=N`.

All list routes use **keyset cursor pagination** — not offset. Pass `next_cursor`
from the previous page verbatim.

***

## List jobs

```
GET /v1/jobs
```

List all jobs for the authenticated tenant, newest first.

**Query parameters:**

* **`workspace_id`** (optional) — Restrict to one workspace. Omit to include document-operation jobs too.
* **`kind`** (optional, repeatable) — Filter by job kind: `parse`, `split`, `classify`, `categorize`, `extract`, `ground`, `ingestion`, `reconciliation`, `kg_build`, `file_upload`, `file_replace`, `file_delete`, `workspace_delete`, `intelligent_search`.
* **`status`** (optional, repeatable) — Filter by status: `queued`, `running`, `succeeded`, `failed`, `cancelled`.
* **`created_after`** (optional) — ISO 8601 timestamp; restrict to jobs created strictly after this time.
* **`cursor`** (optional) — Opaque cursor from a previous page's `next_cursor`.
* **`limit`** (optional, default 50, max 200) — Rows per page.

```bash theme={"dark"}
curl "$NDI_BASE_URL/v1/jobs?kind=ingestion&status=running" \
  -H "X-API-Key: $NDI_API_KEY"
```

**Response:**

```json theme={"dark"}
{
  "items": [
    {
      "job_id": "550e8400-e29b-41d4-a716-446655440012",
      "kind": "intelligent_search",
      "status": "succeeded",
      "workspace_id": "550e8400-e29b-41d4-a716-446655440003",
      "created_at": "2026-08-09T12:00:00Z",
      "finished_at": "2026-08-09T12:02:30Z",
      "units": 4
    }
  ],
  "next_cursor": "eyJjcmVhdGVkX2F0IjogIjIwMjYtMDgtMDlUMTI6MDA6MDBaIiwgImlkIjogIi4uLiJ9",
  "total_count": 42
}
```

***

## Get job

```
GET /v1/jobs/{job_id}
```

Retrieve full details of a job, including the result for terminal jobs.

A late poll always returns the job with a `result_state` — never a 404. "This expired"
and "this never existed" are different conditions, and a 404 would conflate them.

```bash theme={"dark"}
curl "$NDI_BASE_URL/v1/jobs/550e8400-e29b-41d4-a716-446655440011" \
  -H "X-API-Key: $NDI_API_KEY"
```

**Response (running):**

```json theme={"dark"}
{
  "job_id": "550e8400-e29b-41d4-a716-446655440011",
  "kind": "parse",
  "status": "running",
  "progress": {"units_total": 96, "units_done": 40, "stage": "ocr"},
  "created_at": "2026-08-09T11:55:00Z",
  "started_at": "2026-08-09T11:55:05Z"
}
```

**Response (succeeded):**

```json theme={"dark"}
{
  "job_id": "550e8400-e29b-41d4-a716-446655440011",
  "kind": "parse",
  "status": "succeeded",
  "result": {
    "result_type": "parse",
    "file_name": "report.pdf",
    "lane": "document",
    "document": {
      "page_count": 96,
      "markdown": "...",
      "text": null,
      "blocks": null,
      "chunks": []
    },
    "ocr_applied": false,
    "units": 96
  },
  "result_state": "available",
  "units": 96,
  "created_at": "2026-08-09T11:55:00Z",
  "started_at": "2026-08-09T11:55:05Z",
  "finished_at": "2026-08-09T11:57:30Z",
  "payload_expires_at": "2026-09-08T11:57:30Z"
}
```

**Response (failed):**

```json theme={"dark"}
{
  "job_id": "550e8400-e29b-41d4-a716-446655440011",
  "kind": "parse",
  "status": "failed",
  "error": {
    "code": "corrupt_file",
    "message": "File could not be parsed",
    "retryable": false,
    "request_id": "..."
  },
  "created_at": "2026-08-09T11:55:00Z",
  "finished_at": "2026-08-09T11:55:15Z"
}
```

**Job fields:**

* **`kind`** — What the job is doing (see kinds table below).
* **`status`** — `queued`, `running`, `succeeded`, `failed`, or `cancelled`.
* **`force`** — Echoes the request's `force` flag for job kinds that support it, such as `kg_build`; otherwise `null`.
* **`result`** — Present only on `succeeded` and only while retained.
* **`result_state`** — `available`, `expired`, or `not_retained`.
* **`error`** — Present only on `failed`.
* **`progress`** — Present when `running`; includes `units_total`, `units_done`, `items_total`, `items_done`, `stage`.
* **`payload_expires_at`** — When the job's input and result payloads are deleted.

***

## Get a Ground crop

```
GET /v1/jobs/{job_id}/ground-crops/{artifact_ref}
```

Stream a source-image crop produced by a successful Ground job. When Ground is
requested with `options.include_previews: true`, each match that has a visual
region can include a stable `cropped_image_url` pointing to this route.

Use the returned `cropped_image_url` unchanged. The encoded `artifact_ref` is
opaque and must not be constructed by clients. Every request requires the same
API-key authentication, verifies tenant ownership, and confirms that the crop
belongs to the job result.

```bash theme={"dark"}
curl "$CROPPED_IMAGE_URL" \
  -H "X-API-Key: $NDI_API_KEY" \
  --output match.png
```

**Headers:**

* **`Range`** (optional) — One byte range, such as `bytes=0-65535`. A satisfiable range returns `206 Partial Content`.

**Errors:**

* **`file_not_found`** (404) — The job is unavailable, the crop reference is invalid or absent from the Ground result, or its bytes no longer exist.
* **`range_not_satisfiable`** (416) — The requested byte range is invalid.

***

## Cancel job

```
POST /v1/jobs/{job_id}/cancel
```

Cancel a queued or running job. `cancelled` is a first-class terminal status, not a
`failed` variant.

**Request body:**

```json theme={"dark"}
{
  "reason": "No longer needed"
}
```

**Body fields:**

* **`reason`** (optional) — Stored in the job's error payload for the audit trail.

**Response (200):**

```json theme={"dark"}
{
  "job_id": "550e8400-e29b-41d4-a716-446655440011",
  "kind": "parse",
  "status": "cancelled",
  "error": {"code": "job_cancelled", "message": "No longer needed", "retryable": false, "request_id": "..."},
  "created_at": "2026-08-09T11:55:00Z",
  "finished_at": "2026-08-09T11:55:10Z"
}
```

**Errors:**

* **`job_not_found`** (404) — Job does not exist for this key.
* **`job_not_cancellable`** (409) — Job already reached a terminal state.

***

## Stream job events

```
GET /v1/jobs/{job_id}/events
```

Stream real-time job status updates as Server-Sent Events (`text/event-stream`).
Each frame is a `JobEvent` JSON object with a monotonic `sequence` id.

**`JobEvent` fields:**

* **`sequence`** — Monotonic sequence number per job. Use `Last-Event-ID` to resume without gaps.
* **`at`** — Timestamp of this event.
* **`status`** — Job status at this point in time.
* **`progress`** — `null` until running; includes `units_total`, `units_done`, `items_total`, `items_done`, `stage`.
* **`message`** (optional) — Human-readable status message for display.

```bash theme={"dark"}
curl "$NDI_BASE_URL/v1/jobs/550e8400-e29b-41d4-a716-446655440011/events" \
  -H "X-API-Key: $NDI_API_KEY" \
  -H "Accept: text/event-stream"
```

**Output:**

```
id: 1
data: {"sequence":1,"at":"2026-08-09T12:00:00Z","status":"queued","progress":null}

id: 2
data: {"sequence":2,"at":"2026-08-09T12:00:05Z","status":"running","progress":{"units_done":0,"stage":"ocr"}}

id: 3
data: {"sequence":3,"at":"2026-08-09T12:01:00Z","status":"succeeded","progress":null}
```

The stream closes when the job reaches a terminal state or the ceiling (300 s) passes.
Resume a disconnected stream with `Last-Event-ID: <sequence>` to keep numbering monotonic.

For time-sensitive UI updates, prefer streaming. For background polling, use `get_job`.

***

## Job kinds

| `kind`               | Created by                                                                       |
| -------------------- | -------------------------------------------------------------------------------- |
| `parse`              | `POST /v1/parse`                                                                 |
| `split`              | `POST /v1/split`                                                                 |
| `classify`           | `POST /v1/classify`                                                              |
| `categorize`         | Hidden legacy alias `POST /v1/document-operations/categorize`                    |
| `extract`            | `POST /v1/extract`                                                               |
| `ground`             | `POST /v1/ground`                                                                |
| `ingestion`          | `POST /v1/workspaces/{id}/ingestions`                                            |
| `reconciliation`     | `POST /v1/workspaces/{id}/reconciliations`                                       |
| `kg_build`           | `POST /v1/workspaces/{id}/knowledge-graph/builds`                                |
| `file_upload`        | `POST /v1/workspaces/{id}/files`                                                 |
| `file_replace`       | `PUT /v1/workspaces/{id}/files/{file_id}`                                        |
| `file_delete`        | `DELETE /v1/workspaces/{id}/files/{file_id}`                                     |
| `workspace_delete`   | `DELETE /v1/workspaces/{id}`                                                     |
| `intelligent_search` | `POST /v1/workspaces/{id}/deep-search` or `POST /v1/workspaces/{id}/fact-search` |

***

## Polling strategy

```bash theme={"dark"}
# Start a job (no wait)
JOB=$(curl -s -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/r.pdf", "file_name": "r.pdf"}}' \
  | jq -r '.job_id')

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

# Read result
curl -s "$NDI_BASE_URL/v1/jobs/$JOB" -H "X-API-Key: $NDI_API_KEY" | jq '.result'
```

Or use `?wait_seconds=N` to receive the result inline when the job finishes quickly.
