---
title: "Run asynchronously"
canonical_url: "https://www.nutrient.io/guides/dws-data-extraction/runs/async/"
md_url: "https://www.nutrient.io/guides/dws-data-extraction/runs/async.md"
last_updated: "2026-10-21T00:00:00.000Z"
description: "Submit parse, extract, classify, and split requests as queued runs, poll for the result, cancel runs, and retry safely with an Idempotency-Key."
---

# Run asynchronously

The parse, extract, classify, and split endpoints provide asynchronous routes. These routes accept the same request formats and return a processing run instead of the result:

```

POST https://api.nutrient.io/extraction/parse/async
POST https://api.nutrient.io/extraction/extract/async
POST https://api.nutrient.io/extraction/classify/async
POST https://api.nutrient.io/extraction/split/async

```

The API returns `202 Accepted` with the run in the `queued` state. Poll the run until processing finishes. If the run succeeds, download its result asset.

Asynchronous routes process long documents and batches without keeping connections open. They also support clients that can’t wait for processing, such as webhook handlers and queue workers.

For run details and response fields, refer to the [processing runs](https://www.nutrient.io/guides/dws-data-extraction/runs.md) guide.

## Prerequisites

Your workspace needs the asynchronous runs feature enabled. Asynchronous splitting also requires the Split feature. The API returns HTTP 404 when a required feature isn’t enabled.

## Submit a run

Use the synchronous endpoint’s request format with the asynchronous path. Send a multipart upload with `file` and `instructions`, or a JSON body with `url`.

Asynchronous routes also accept an `assetId` from `POST /extraction/uploads` instead of a file or URL.

The following examples submit a parse request:

### curl

```shell

curl -i -X POST https://api.nutrient.io/extraction/parse/async \
  -H "Authorization: Bearer your_api_key_goes_here" \
  -F "file=@document.pdf" \
  -F 'instructions={"mode":"agentic"}'

```

### Python

```python

import requests

with open("document.pdf", "rb") as file:
    response = requests.post(
        "https://api.nutrient.io/extraction/parse/async",
        headers={"Authorization": "Bearer your_api_key_goes_here"},
        files={"file": file},
        data={"instructions": '{"mode":"agentic"}'},
    )

if response.status_code!= 202:
    raise RuntimeError(f"HTTP {response.status_code}: {response.text}")

run = response.json()
print(run["id"], run["status"])

```

### JavaScript

```javascript

import fs from "node:fs";

const form = new FormData();
form.append("file", new Blob([fs.readFileSync("document.pdf")]), "document.pdf");
form.append("instructions", JSON.stringify({ mode: "agentic" }));

const response = await fetch("https://api.nutrient.io/extraction/parse/async", {
  method: "POST",
  headers: { Authorization: "Bearer your_api_key_goes_here" },
  body: form,
});

if (response.status!== 202) {
  throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}

const run = await response.json();
console.log(run.id, run.status);

```

The HTTP 202 response includes the run ID in its body and the following headers:

```

HTTP/2 202
location: /extraction/runs/7KPSBBDRPEZ510NKVN0E42VHBH
x-nutrient-run-id: 7KPSBBDRPEZ510NKVN0E42VHBH

```

The body contains the same run details as `GET /extraction/runs/{runId}`. A new run has `status: queued`, empty `usage`, and only the `input` asset.

The API checks credits when you submit a request. Insufficient credits return HTTP 402 without queuing a run. The API charges for the run when processing finishes.

The API stores every asynchronous run. It rejects `storeRun: false` in the instructions with HTTP 400.

## Poll for the result

Poll the run until its `status` is `done`, `error`, or `canceled`. Download the `result` asset only when the status is `done`:

### curl

```shell

curl https://api.nutrient.io/extraction/runs/7KPSBBDRPEZ510NKVN0E42VHBH \
  -H "Authorization: Bearer your_api_key_goes_here"

curl https://api.nutrient.io/extraction/runs/7KPSBBDRPEZ510NKVN0E42VHBH/assets/result \
  -H "Authorization: Bearer your_api_key_goes_here" \
  -o result.json

```

### Python

```python

import time

import requests

headers = {"Authorization": "Bearer your_api_key_goes_here"}
run_url = f"https://api.nutrient.io/extraction/runs/{run['id']}"

delay = 2
while True:
    run = requests.get(run_url, headers=headers).json()
    if run["status"] in ("done", "error", "canceled"):
        break
    time.sleep(delay)
    delay = min(delay * 2, 30)

if run["status"]!= "done":
    raise RuntimeError(f"run ended {run['status']}: {run['error']}")

result = requests.get(f"{run_url}/assets/result", headers=headers).json()
print(result["output"])

```

### JavaScript

```javascript

const headers = { Authorization: "Bearer your_api_key_goes_here" };
const runUrl = `https://api.nutrient.io/extraction/runs/${run.id}`;

let delay = 2000;
let settled;
while (true) {
  settled = await (await fetch(runUrl, { headers })).json();
  if (["done", "error", "canceled"].includes(settled.status)) break;
  await new Promise((resolve) => setTimeout(resolve, delay));
  delay = Math.min(delay * 2, 30000);
}

if (settled.status!== "done") {
  throw new Error(`run ended ${settled.status}: ${JSON.stringify(settled.error)}`);
}

const result = await (await fetch(`${runUrl}/assets/result`, { headers })).json();
console.log(result.output);

```

Wait at least two seconds between polling requests. Increase the interval while processing continues.

Polling and submission requests share a rate limit, so frequent polling reduces capacity for new submissions. Frequent polling can exhaust a test key’s limit within seconds.

While the run is `running`, the response includes a `progress` object with the worker’s latest report. It contains the current `phase`, a `percent` estimate when available, and page counters.

Use `progress` to display processing progress, not to determine the outcome. Continue polling until `status` is `done`, `error`, or `canceled`. For field details, refer to the [processing runs](https://www.nutrient.io/guides/dws-data-extraction/runs.md) guide.

The `result` asset contains the response body from the synchronous endpoint. Its fields depend on the endpoint.

For the `output` format, refer to the [parse](https://www.nutrient.io/guides/dws-data-extraction/parsing.md), [extract](https://www.nutrient.io/guides/dws-data-extraction/extract.md), [classify](https://www.nutrient.io/guides/dws-data-extraction/classify.md), and [split](https://www.nutrient.io/guides/dws-data-extraction/split.md) guides.

Split results depend on the selected output format:

- With `json` or `both`, the result includes page ranges.

- With `zip` or `both`, the run includes a `zip` asset, and `output.downloadUrl` in the result points to it.

- With `zip`, the result omits page ranges.

## Read the outcome

The final status identifies the outcome and whether the API charged for the run:

| Status     | What it means                                                                                                                                                                                                                                                   |
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `done`     | The `result` asset contains the output. The API charged once, and `usage.data_extraction_credits.cost` reports the amount.                                                                                                                                      |
| `error`    | No result. Check `error.status` and `error.details.source`. Status `402` with source `billing` means processing finished, but billing failed. The API withholds the result and charges nothing. Other errors mean processing failed without a result or charge. |
| `canceled` | You canceled the run before completion. The API discarded any later engine output and charged nothing.                                                                                                                                                          |

You can resubmit a failed run. If you used an idempotency key, use a new one. Failed runs retain their keys until deletion, so reusing the key returns the failed run instead of creating another.

Each run has a 15-minute processing limit. Runs that exceed this limit end with status `error` and incur no charge. Split longer documents into smaller files. Submit each file separately.

Workers automatically resume interrupted runs. Repeated interruptions end the run with status `error`, and the API charges nothing for those attempts.

## Cancel a run

Cancel a run that hasn’t finished:

```shell

curl -X POST https://api.nutrient.io/extraction/runs/7KPSBBDRPEZ510NKVN0E42VHBH/cancel \
  -H "Authorization: Bearer your_api_key_goes_here"

```

The API returns HTTP 200 with the run’s status set to `canceled`. Cancellation stops a `queued` run before processing starts.

For a `running` run, the engine continues processing, but the API marks the run `canceled` and discards the result. The API doesn’t charge for canceled runs.

Canceling an already canceled run returns HTTP 200 again. Canceling a run with status `done` or `error` returns HTTP 409 with code `run_not_cancelable`.

The API rejects deletion requests for `queued` or `running` runs. Cancel the run before deleting it.

## Retry safely with an idempotency key

A network failure can prevent you from receiving HTTP 202 after the API accepts your request. Retrying without an idempotency key can create duplicate runs and charges.

Include an `Idempotency-Key` header when submitting the request:

```shell

curl -i -X POST https://api.nutrient.io/extraction/parse/async \
  -H "Authorization: Bearer your_api_key_goes_here" \
  -H "Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7" \
  -F "file=@document.pdf" \
  -F 'instructions={"mode":"agentic"}'

```

The key contains 1 to 255 printable characters. Its scope covers your account and the operation, so the same key identifies separate runs for parse and extract.

Retrying the same request with the same key returns HTTP 202 without creating another run. The response includes the original run ID and an `Idempotency-Replayed: true` header. Reusing the key for a different request returns HTTP 409 with code `idempotency_key_reused`.

The request’s identity includes the instructions, engine version, processor reference, and input bytes. For `url` input, the API downloads the document on every submission, including retries.

The API replays a request only if the URL returns the same bytes. Changed content causes HTTP 409. If the URL no longer resolves, the submission fails.

For URLs whose content can change, retain the run ID from the first HTTP 202 response. Poll that run instead of resubmitting the request. Alternatively, upload the file.

The key identifies the request, not an individual attempt. Canceled, failed, and expired runs retain their keys until deletion, so retries return the existing run.

To process the request again, use a new key or delete the existing run to release its key. Synchronous endpoints ignore the header.

## Errors

Asynchronous requests return the same validation and billing errors as the synchronous endpoints, plus the errors below:

| Status | Code                     | Meaning                                                                                        |
| ------ | ------------------------ | ---------------------------------------------------------------------------------------------- |
| 400    | `invalid_request`        | `storeRun: false`, or an `Idempotency-Key` that doesn’t contain 1 to 255 printable characters. |
| 402    |                          | Insufficient credits or run storage. The API didn’t queue the request.                         |
| 409    | `idempotency_key_reused` | Another request already uses this `Idempotency-Key`.                                           |
| 409    | `run_not_cancelable`     | The run you tried to cancel has already finished.                                              |
| 409    | `run_in_flight`          | The run you tried to delete is still queued or running. Cancel it first.                       |
| 410    | `run_expired`            | The run has passed its retention window.                                                       |
| 503    | `run_enqueue_failed`     | The API couldn’t queue the run and stored nothing. Submit the request again.                   |

Billing errors can include `error`, `code`, and `message` instead of `errorMessage`. Use the HTTP status to detect submission failures.

Refer to the [error handling](https://www.nutrient.io/guides/dws-data-extraction/errors.md) guide for response formats.

## Next steps

Use these references for run management and response details:

- Refer to the [processing runs](https://www.nutrient.io/guides/dws-data-extraction/runs.md) guide for reading, listing, and deleting runs, and for retention.

- Refer to the [error handling](https://www.nutrient.io/guides/dws-data-extraction/errors.md) guide for status codes and error response formats.

- Refer to the [API reference](https://www.nutrient.io/api/reference/data-extraction/public/) for the full request and run schemas.
---

## Related pages

- [Processing runs](/guides/dws-data-extraction/runs.md)

