Parsing
Parsing turns a document into a bundle of artifacts: markdown you can read, JSON you can ground, page images you can draw on, and cropped figures you can show. Everything else in DocAI — extraction, search, agents — reads that bundle.
Before you start. Parsing runs on a file that is already uploaded. Get an API key and upload a document with the quickstart; the file has to sit in a knowledge base first.
A parse is queued, not synchronous. You get a job id, you poll it, and when it completes you fetch the artifacts by name.
Code
Code
The response is 202. Credits are reserved up front at one credit per page and
finalised against the pages actually processed. The reserve-then-commit flow and
the monthly quota are covered in the quickstart. A short
balance fails the dispatch with 402 insufficient_credits, and a capped
organization with 402 monthly_quota_exceeded, before any work is queued —
those and the rest are on the error codes page.
Full request and response shape: queue parse job for file in the API reference.
Idempotency
The default idempotency key is parse:{file_version_id}:{options_hash}. Sending
the same request twice returns the first job with idempotent: true rather than
queuing a second one and charging you twice. Pass your own idempotency_key to
override it.
A terminal job that failed is retryable: dispatching again creates a fresh job
and reports the previous one as retry_of_job_id.
What a parse produces
One parse writes one artifact manifest.
GET /v1/files/{fileId}/artefacts
returns every manifest for the file, newest first, each with a links object:
Code
Note the spelling. The listing endpoint is /artefacts with an e. The
download endpoint is /artifacts with an i. That is not a typo in this page;
it is the actual API. Get it wrong and you get a 404 with no hint.
A null link means this parse did not produce that artifact. Branch on the
link, not on the file type.
The artifact keys
| Key | Type | Contains | Present when |
|---|---|---|---|
result_md | Markdown | The readable document: headings, paragraphs, tables, image references | Always |
grounding_json | JSON | Every element with its page, bounding box, label and content | Always |
pages | Directory | Rendered page images, page_0001.png upward, 150 dpi by default | PDFs and images |
imgs | Directory | Cropped figures, charts, tables and formulas as JPEG | When the document has any |
workbook_json | JSON | Sheets, rows and cells of a spreadsheet | Spreadsheets only |
tables_json | JSON | Detected used-range tables in a spreadsheet | Spreadsheets only |
result_json | JSON | Structured extraction output | Extract manifests, not parse |
result_json belongs to artifact_type: "extract". It is listed here because it
appears in the same links object and people reach for it expecting parse
output. On a parse manifest it is normally null.
Fetching one
Code
Links are built from manifest ids and artifact keys, never from filesystem
paths. There is no path to guess and nothing to traverse. The endpoint is
download or list an
artefact.
A manifest id the organization does not own is 404 artefact_manifest_not_found, and a key the manifest does not carry is 404 artefact_not_found — see error codes.
pages and imgs are directories. Fetching one without a file query returns
a listing:
Code
Then fetch a single entry with ?file=page_0001.png. A file value containing
.. is rejected with 403 invalid_artefact_child_path.
Images in the markdown are relative
result_md references crops as relative paths, for example
. Those paths mean nothing to
your renderer. Rewrite each imgs/NAME to
/v1/artifacts/{manifestId}/imgs?file=NAME before displaying the markdown, or
the images will silently fail to load.
Backend routing
backend defaults to auto. Auto is not a guess — it is a strict test, and it
is deliberately biased towards OCR.
A PDF goes to Docling only when every one of these holds, on every page:
- the embedded text layer has at least 150 characters on that page — a per-page floor, not a document average
- images and vector drawings cover less than 10% of the page area
- the document has no AcroForm or XFA form fields
- no page carries a stamp annotation
Fail any one of them, on any single page, and the whole document goes to OCR.
Plain images (.png, .jpg, .jpeg) and every non-PDF input go straight to
OCR without inspection.
A figure-heavy born-digital PDF will take the OCR path. A clean, text-native brochure with a large hero image on page one crosses the 10% graphic threshold and routes to OCR. That is policy, not a bug: Docling reads the text layer, and on documents that are substantially visual its text layer is not the document. One page over the line takes the whole file with it, because a bundle assembled from two different backends has two different notions of what an element is.
Force a backend when you know better:
Code
Accepted values are auto, docling and glm-ocr. Other options passed
through to the parser include dpi (default 150), layout_device,
page_fallback_ocr and timeout_seconds. Options are part of the idempotency
key, so changing one queues a genuinely new parse.
Re-parsing is page-diffed
Before parsing, DocAI renders the pages and hashes them against the previous
version. If nothing changed and the previous artifacts are readable, the job
completes at stage: "reused" with percent: 100 and debits zero credits. Only
changed pages are charged on a partial re-parse — manifest.changed_pages and
manifest.unchanged_pages tell you which were which.
Groundings and coordinates
grounding_json is the parse in structured form:
Code
bbox is fractional, 0–1 of the page. pixel_bbox is absolute pixels at the
rendered resolution. Both describe the same box.
Segments
Rather than parsing that yourself, use the segments endpoint. It flattens the grounding into a list ready for citation:
Code
The response is a bare array, not an envelope:
Code
Segments with no markdown are dropped, so an empty array means the parse produced no readable content, not that the file is missing.
The 0–1000 coordinate scale
coordinates is normalised to integers from 0 to 1000, origin top-left. It
is neither pixels nor fractions. A fractional grounding box is multiplied by
1000 and rounded; a pixel box is clamped into the same range. Values are always
clamped to [0, 1000].
To draw a highlight over a rendered page image of width W and height H:
Code
A DocAI segment is one grounded piece of text with a page number and a
bounding box. pageNumber is 1-based and matches page_0001.png for page 1.
Pair a segment with its page render and you have a citation a
user can see: the page, the box, and the text that produced the answer. Segment
ids are also the join key for extraction citations — see
extraction.
Table cell geometry
Table segments may carry an additive cells array — one entry per logical
cell, {row, col, rowSpan, colSpan, bbox, text, isHeader}. Merged cells appear
once with a span greater than 1, so never assume a span-1 grid.
Three fields tell you how far to trust that geometry:
cellsSource: "table-former"— cells come from the TableFormer structure model. Measured per-cell boxes, real spans, both axes trustworthy. This is the preferred source.cellsSource: "projection"— both axes were confidently detected from the crop by grid detection.cellsSource: "grid-estimate"— at least one axis fell back to a uniform split.cellGrid: {rows, cols}— per-axis confidence, each"detected"or"estimated", decided independently.cellsEstimateReason— present only on the scanned path, and only when the TableFormer pass ran and declined, naming why:"col count mismatch: html=15 model=2","tableformer model unavailable","crop too small: 40x12px".
The rule for a client is cellGrid, not cellsSource:
Draw a precise per-cell box only when
cellGrid.cols === "detected". When it is"estimated", highlight the whole row band instead of a specific column.cellGrid.rowsfollows the same rule, though rows are detected on the large majority of real tables.
Columns are conservatively marked "estimated" on borderless or
mixed-alignment tables rather than reported as precise. That is deliberate: a
cell box that looks exact but sits in the wrong place is worse than an honest
estimate. On a dense 15-column form, TableFormer has been measured detecting
only 2 columns, which is why the structure pass refuses to emit a grid that
disagrees with the OCR text grid.
cellsEstimateReason is diagnostic. Put it in a debug view or a support report,
not in front of an end user. Its absence on a grid-estimate table means the
structure pass never ran at all — the Docling path, or a non-table element.
Spreadsheets
.xlsx and .csv take a different path. There are no pages, so the unit is
sheets: manifest.unit is "sheets", manifest.source_type is
"spreadsheet", and credits are debited per sheet. The bundle carries
workbook_json and tables_json instead of page renders — list them with
GET /v1/files/{fileId}/artefacts
and download them through the artifact links it returns, like any other
artifact in the bundle.
A spreadsheet only counts as fully parsed once it has both a workbook_json
link and a non-zero segment_count. A manifest without them is stale.
Page images
Every parsed page is rendered to pages/page_XXXX.png inside the artifact
bundle. List them with GET /v1/files/{fileId}/artefacts
— the manifest's pages directory carries one entry per page — and download
each through its artifact link:
Code
Filenames are zero-padded, so lexical order is page order, and pageNumber on
a segment indexes straight into them. Spreadsheets have no pages to render —
read the sheet through its workbook_json and tables_json artifacts
instead.
Bulk parse
Queue parse jobs for multiple files:
Code
Returns 202 with one dispatch entry per file, in request order:
Code
Bulk parse is not atomic, and this costs money. Files are dispatched one at a
time, in order. If the fifth file id does not exist, the request fails with
404 file_not_found — and the first four jobs are already queued
with credits already reserved against them. The error response does not tell you
which ones went through.
Two habits avoid the mess:
- Validate every file id before sending the batch.
- Pass an
idempotency_key. Each file gets{your_key}:{file_id}, so a retry after a partial failure re-attaches to the jobs that already exist instead of queuing and charging for them a second time.
options applies to every file in the batch. Mixed batches needing different
backends need separate calls.
Progress and failure
A parse is a job, polled on the file. Queued document workflows — split, classify, schema generation — are operations with a different identifier and a different poll endpoint; see workflows before you mix the two.
Code
status is one of queued, running, completed, failed, cancelled.
Internal names such as started and in_progress are normalised to running;
raw_status carries the unnormalised value if you need it.
percent is monotonic and banded by stage:
| Percent | Stage | Meaning |
|---|---|---|
| 5 | page-diff | Hashing pages against the previous version |
| 10 → 75 | parse | Text extraction — Docling chunks or GLM-OCR regions |
| 75 → 95 | parse | Visual analysis of figures and charts |
| 100 | completed | Done |
| 100 | reused | Nothing changed; previous artifacts kept, no credits spent |
Spreadsheets use parse-spreadsheet in place of parse.
Use percent for progress bars. completed_pages and total_pages are
written only at stage boundaries, so they sit still while work is happening.
Progress is a display signal, not a contract: a parser that reports nothing
leaves percent at its last stage boundary.
When it fails
status: "failed" puts the reason in error. The event
log has the
detail:
Code
Code
Events are ordered oldest first. A job.failed event carries the last 4000
characters of the traceback in payload.traceback, which is usually enough to
tell a bad input from an infrastructure problem.
Two failures worth recognising by their message:
- Killed by the container runtime. The message names insufficient Docker memory. On-prem only. Raise the memory limit and retry; the input is fine.
- "Parser completed but expected artefacts were not found". The parser exited cleanly but wrote nothing. Usually a source file that is not what its extension claims.
A failed parse spends nothing — the credit reservation is released rather than
debited. Retrying is a normal dispatch: same endpoint, and the new job reports
retry_of_job_id.
Failed jobs stay in the history until you clear them:
Code
Code
That removes failed parse and extract jobs and their events for the file. It
does not touch the file, its versions, or any successful parse. It needs
manager access on the file, where polling only needs viewer.
Where to next
- Extraction — turn the parse into JSON that matches a schema, with citations back to the segments above.
- Knowledge bases — search what you have parsed, and index a whole corpus rather than one file at a time.
- Split, Classify and the other workflows that run on a parsed document.
- Error codes — every code a parse dispatch or poll can return.

