
# Async jobs

Some work can't finish inside a single request — a multi-page document analysis runs for minutes, in a separate worker. Those ops return a **job ticket** immediately instead of a result. You then **poll** the job or wait for a **webhook**; when it's done, the result is inline on the job or delivered to a baton you named.

## The shape

1. **Submit.** Call the async op (the first is multi-page Textract — [`POST /v1/doc/analyze-async`](/docs/docai)). It returns a ticket:

   ```json
   { "ticket": "...", "job_id": "...", "status": "running", "ceiling_micros": 42000 }
   ```

2. **Wait.** Either **poll** `GET /v1/jobs/{id}` until the status is terminal, or subscribe to the **`job.completed`** webhook and let it push you the finished job. (Polling and the webhook are two ways to learn the same thing — use whichever fits your runtime.)

3. **Collect.** When the job succeeds, the result is available on the job record — inline, or, if you passed a `deliver: { batonId }` at submit, written into that baton.

## Statuses

`GET /v1/jobs/{id}` reports one of five statuses:

| Status | Meaning |
|---|---|
| `submitted` | accepted, not yet picked up |
| `running` | the worker is processing it |
| `succeeded` | done — the result is available |
| `failed` | a terminal failure (the charge is made whole) |
| `expired` | the job could not be admitted / charged, or aged out |

`submitted` and `running` are the in-flight states; `succeeded`, `failed`, and `expired` are terminal — once a job reaches one, it doesn't change.

## Billing, in plain terms

The counted work (for Textract, the page count) is measured **at submit**, so the ceiling is known up front. How it settles depends on your payment rail:

- **x402 (lodestone).** The counted ceiling settles **at submit** — one clean on-chain settle. The compute finishes later; only the result is deferred, not the payment. If the job then fails for an infrastructure reason, the shortfall is made good off-chain.
- **Balance / API key.** A **hold** is authorized at submit, and the **actual** is captured at completion (the result is delivered before the capture). A failed job **releases the hold** — you're made whole.

Either way: a job that fails for a reason on our side does not cost you. See [Receipts and cost-plus](/docs/receipts) for how the ceiling-vs-charged figures read on the receipt.

## First consumer

The first async op is **multi-page Textract** — [`POST /v1/doc/analyze-async`](/docs/docai) in [Document AI](/docs/docai). More long-running ops will join this lane; they all speak the same ticket → poll / webhook → result contract.

## Next

[Document AI](/docs/docai) · [Webhooks](/docs/webhooks) · [Receipts and cost-plus](/docs/receipts) · [Quickstart](/docs/quickstart) · [API reference](/api-reference)
