Jobs
The single status surface for everything asynchronous.
Anything the API does asynchronously hands back a job handle — an opaque, signed jobId — and GET /jobs/{jobId} is the one place to read its status. Two calls return one: POST /documents/process (a batch preprocess job over the documents in that call) and POST /extraction-sessions (an extraction job). The document.* webhooks also carry a single-document preprocess handle.
Handles are signed. A forged, unknown, expired or cross-organization handle answers 404, indistinguishably, so the endpoint is not an existence oracle.
Status
One status set for every job type:
queued → in_progress → completed | completed_with_failures | failed | cancelled
POST /jobs/{jobId}/cancel requests cancellation. A preprocess job is not terminal-sticky: force-reprocessing any document it covers reopens it from completed to in_progress.
Progress and results
Batch-preprocess and extraction jobs carry progress { done, total, failed } and a per-document result.documents array, so the default response says which document failed. The two types differ in row shape — preprocess rows are { documentId, status, error? }, extraction rows are { documentId, status } and never carry error — and in status vocabulary: an extraction row can be queued, a preprocess row cannot. Once an extraction completes, result.results carries the session id to read results from.
A single-document preprocess job has no progress; it carries fileName and numPages instead. duration is seconds and appears only once terminal — a document's own wall-clock, the maximum across a batch, or created-to-completed for a session. Pass ?expand=documents to add fileName, numPages and reference to each breakdown row.
Polling or webhooks
Polling GET /jobs/{jobId} is cheap and rate-limited like any other call. For anything longer than a few seconds, subscribe a webhook endpoint instead: extraction.completed and extraction.failed fire once per session, document.processed and document.processing_failed per document, and every payload carries the same jobId so you can fetch the outcome from here rather than trusting the payload.