Polling
GET /v1/jobs/{id} is the only endpoint — it returns both the job
status and (once complete) the result in the same response; there is no
separate "get result" endpoint. Even if webhook delivery fails or never
arrives, the client can always rely on this endpoint: polling is the
reliable fallback for webhooks.
State machine
queued -> processing -> parsing -> indexing -> completed / failed
| State | Meaning |
|---|---|
queued | Document received, waiting in queue. |
processing | Job started; the document is being split into pages (the first step of the page-by-page resilient pipeline). |
parsing | Pages are being extracted one by one. Even if a page temporarily fails, the job does not get stuck — automatic retry and last-resort fallback are in effect; the client doesn't need to do anything. |
indexing | All pages are merged in page_no order to produce the final markdown/JSON (the full-text search index is also written at this step). |
completed | The result is ready; the result field is populated. |
failed | The job has permanently failed; the error field is populated, result is always null. |
The result field is only populated when status: "completed". The
error field is only populated when status: "failed".
Once a document becomes completed, it automatically enters AI-indexing
(chunk -> embed -> vector) as well as AI-representation production
(memory, entities, reasoning, layout — see Extraction &
Artifacts); this proceeds in the background,
independently of completed. Indexed documents become queryable via
Semantic Search and Q&A.
Progress of this background phase is exposed via the additive
ai_status field on this same response (see below) — a job can read
status: "completed" while ai_status is still chunking or
embedding.
GET /v1/jobs/{id} — status and result in the same response
{
"id": "d_9c1a...",
"batch_id": "b_5f2a...",
"external_item_ref": "INV-001",
"external_batch_ref": "PO-2026-001",
"status": "completed",
"page_count": 12,
"partial": false,
"file_size_bytes": 245678,
"source": {
"filename": "invoice-001.pdf",
"content_type": "application/pdf",
"size_bytes": 245678,
"url": "https://media-cdn.../signed?exp=..."
},
"result": {
"markdown_url": "https://markdown-cdn.../result.md?signed&exp=...",
"json_url": "https://markdown-cdn.../result.json?signed&exp=...",
"structured_url": "https://markdown-cdn.../result.structured.md?signed&exp=..."
},
"error": null,
"created_at": "2026-07-01T09:00:00Z",
"updated_at": "2026-07-01T09:02:15Z",
"completed_at": "2026-07-01T09:02:15Z",
"ai_status": "ready",
"cost": "16.6893"
}
ai_status— additive field tracking the background AI-representation pipeline for this document, independent ofstatus. One ofnone(not yet started, or briefly right aftercompleted),chunking,embedding,ready(all requested AI artifacts finished),partial(some artifacts failed to produce — the rest are still usable), orfailed. PollGET /v1/documents/{id}/artifacts(see Extraction & Artifacts) to see exactly which artifacts are available.source.url— the download link for the originally uploaded file. If the raw file is deleted from media-cdn after the retention period ends,urlreturnsnull(the file no longer exists, but the record andresultare unaffected).result.markdown_url,result.json_url— time-limited signed links, freshly generated on everyGETcall (lazy signing). Do not cache the link, it expires.result.structured_url— optional; the raw markdown reorganized (structured) according to a fixed template. Only present when it can be generated — it's a best-effort enrichment, not a core deliverable; if it cannot be generated, the field is entirely absent from the response.
Error: 404 not_found — the id doesn't exist or belongs to another
tenant (the resource's existence is never leaked via 403).
Result content: markdown, JSON, and page citations
The two (optional third) links in the result field download different
bodies:
-
markdown_url— plain-text markdown with all pages merged together. -
json_url— the DIP canonical, versioned wrapper schema. Per-page citations (pages[]) are inside this body — not in the job status response:{ "schema_version": "1.0", "source": { "engine": "vlm", "engine_version": "per-page", "backend": "vlm-page" }, "document": { "page_count": 12, "pages": [ { "page": 1, "markdown": "...", "blocks": [] }, { "page": 2, "markdown": "...", "blocks": [] } ], "metadata": {} }, "markdown": "...", "extracted_at": "2026-07-01T09:02:15Z" }Each
pages[]element carries which page it came from (page) and that page's markdown — use this body for page-level citation/source display. -
structured_url(if present) — the same content reorganized according to a fixed template (Parties / Main Table / Total / Other); it is not a summary, no data from the source is dropped.
partial — partial success
In the page-by-page resilient pipeline, a temporary issue on a single
page does not make the whole document failed: failed pages are
automatically retried, falling back to an alternative engine as a last
resort. However, if a page is exhausted across all engines:
- The document still becomes
completed(the other pages are never lost). partial: trueis returned — indicating that at least one page in the result is missing/a placeholder (that page'smarkdowninpages[]is empty).partial: falsemeans a complete result.
In flows requiring high accuracy, treat results with partial: true as
"may be incomplete" and check which page is affected via pages[].
Failed job (failed)
erroris populated,resultis alwaysnull.- On transient errors (e.g., a storage write failure), the job is
automatically retried a limited number of times in the background; the
status only moves to
failedonce retries are exhausted. failedis permanent — retrying on the client side means re-uploading the document (POST /v1/documents).
GET /v1/batches/{id} — batch status
Returns the status of a batch and all documents within it. Batch
status is a derived field: if all documents are completed/failed,
the batch is completed/failed; if at least one is still processing,
it's processing. Each document is listed with the same short
representation as GET /v1/jobs/{id} (id, external_item_ref,
status); for the full result, GET /v1/jobs/{id} must still be called
with the relevant document.id.
Recommended polling strategy
- Initially check every 2-3 seconds, repeating until you reach a
terminal status such as
completed/failed. - If you get
429, honor theRetry-Afterheader and apply exponential backoff (see the rate limit section in Getting Started & Auth). The status-query endpoint's limit is much more generous than uploads, but still avoid tight-loop polling. - If you're receiving Webhooks, use polling only for
verification/fallback — both channels will converge to the same final
state; if webhook delivery is exhausted (
exhausted), the client should already be falling back to this endpoint.