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

> Wait, stream, cancel, inspect, delete, and fetch result artifacts

Every slow `/v1` call returns a `Job`. `wait_seconds` is a query parameter.
`Idempotency-Key` is a header.

<Tabs>
  <Tab title="Python">
    ```python theme={"dark"}
    from ndi_sdk import NdiClient

    with NdiClient() as client:
        upload = client.documents.create_upload("report.pdf")
        queued = client.documents.parse(upload)
        job = client.jobs.wait(queued.job_id, timeout=600)
        print(job.status, job.result.markdown)

        for event in client.jobs.events(job.job_id):
            print(event.status, event.message)
    ```
  </Tab>

  <Tab title="TypeScript">
    ```ts theme={"dark"}
    import { NdiClient } from "ndi-sdk";

    const client = new NdiClient();
    const upload = await client.documents.createUpload("report.pdf");
    const queued = await client.documents.parse(upload);
    const job = await client.jobs.wait(queued.job_id, { timeout: 600 });
    console.log(job.status, job.result);

    for await (const event of client.jobs.events(job.job_id)) {
      console.log(event);
    }
    ```
  </Tab>

  <Tab title="curl">
    ```bash theme={"dark"}
    JOB_ID="550e8400-e29b-41d4-a716-446655440003"

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

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

## Inspect and cancel

| Action       | Python                   | TypeScript              | HTTP                        |
| ------------ | ------------------------ | ----------------------- | --------------------------- |
| Get          | `jobs.get`               | `jobs.get`              | `GET /v1/jobs/{id}`         |
| List         | `jobs.list` / `iter_all` | `jobs.list` / `iterAll` | `GET /v1/jobs`              |
| Cancel       | `jobs.cancel`            | `jobs.cancel`           | `POST /v1/jobs/{id}/cancel` |
| Echo request | `jobs.request`           | HTTP only               | `GET /v1/jobs/{id}/request` |
| Soft-delete  | `jobs.delete`            | HTTP only               | `DELETE /v1/jobs/{id}`      |
| SSE          | `jobs.events`            | `jobs.events`           | `GET /v1/jobs/{id}/events`  |

`jobs.request` returns the document-operation payload a finished run was asked
for. It raises `invalid_request` for kinds that have no single document
request (search jobs) and `result_expired` when retention dropped the payload.

`jobs.delete` hides the row from lists. The job still reads by id with
`deleted_at` set, and its units still count toward usage. A running job is
cancelled first.

`PATCH /v1/jobs/{id}` is REST-only (no SDK wrapper).

## Artifacts

Authenticated fetches, same API key:

* Ground crops — `jobs.ground_crop` / `GET /v1/jobs/{id}/ground-crops/{ref}`
* Ground transcript
* Converted PDF
* Spreadsheet cell maps

When `payload_expires_at` passes, `GET /v1/jobs/{id}` still exists but
`result` is gone (`result_expired`).

## Usage

```python theme={"dark"}
from datetime import date

from ndi_sdk import NdiClient

with NdiClient() as client:
    usage = client.jobs.usage(
        period_start=date(2026, 8, 1),
        period_end=date(2026, 9, 1),
        group_by="day",
    )
    print(usage)
```

`GET /v1/usage/overview` is HTTP-only.

See [Jobs and idempotency](/concepts/jobs-idempotency) and
[Processing units](/concepts/units).
