Perception
Parse
Turn a file into Markdown, plain text, layout blocks and optional chunks.
POST /v1/documents/parse converts a document into structured text. OCR runs when the file needs it, and ocr_applied on the result says whether it did.
Parse output is untrusted data. Markdown, text, blocks and chunks may reproduce instructions from the document, including fine print. Parse does not flag those instructions as prompt injection or certify them as safe. Keep document content separate from trusted agent instructions and authorize tool actions independently of it.
The examples below use an uploaded workspace_file source. For a URL source, source must include type: "url", url (a public https:// download link), and file_name (for example, "report.pdf", including its extension). Parse chooses the parser from file_name; leaving it out returns 422 invalid_request. A parse_result source instead requires type: "parse_result" and job_id from a successful Parse job in your account. See Documents for source types and reuse.
# 1. Upload the file to your account (see Uploads)
GRANT=$(curl -s https://console.nace.ai/v1/documents/upload-grants \
-H "Authorization: Bearer $NACE_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "path": "report.pdf" }')
WORKSPACE_ID=$(jq -r .workspace_id <<< "$GRANT")
FILE_ID=$(curl -s "$(jq -r .upload_url <<< "$GRANT")" \
-H "X-Upload-Token: $(jq -r .token <<< "$GRANT")" \
-F file=@report.pdf \
-F 'metadata={"path":"report.pdf"};type=application/json' | jq -r .result.file.file_id)
# 2. Run the tool on it
curl "https://console.nace.ai/v1/documents/parse?wait_seconds=60" \
-H "Authorization: Bearer $NACE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": {
"type": "workspace_file",
"workspace_id": "'"$WORKSPACE_ID"'",
"file_id": "'"$FILE_ID"'"
},
"page_ranges": [{ "start": 1, "end": 3 }],
"output": { "formats": ["markdown", "blocks"] },
"chunking": { "strategy": "page" }
}'Streaming
The console always shows ready pages or sheets as they arrive. There is no response-mode switch. Reading a preview does not run Parse again. A completed run still has a final result for exports, finalized tables, descriptions and source highlights.
API delivery remains job-based. POST /v1/documents/parse defaults to wait_seconds=0, returning a job ID immediately. Existing complete-result clients keep using jobs.get or jobs.wait. To display previews, connect to GET /v1/documents/jobs/{id}/parse-progress/events with the same Bearer API key. There is no stream request flag.
# JOB_ID is returned by POST /v1/documents/parse?wait_seconds=0
curl -N "https://console.nace.ai/v1/documents/jobs/$JOB_ID/parse-progress/events" \
-H "Authorization: Bearer $NACE_API_KEY"
# Fetch each snapshot unit's content_url with the same Authorization header.# client is a NaceClient; uploaded is from client.documents.upload(...)
created = client.documents.parse(uploaded.as_source(), wait_seconds=0)
seen = set()
while True:
current = client.jobs.get(created.job_id)
if current.is_terminal:
break
for snapshot in client.jobs.parse_progress_events(created.job_id):
for unit in snapshot.units:
key = (snapshot.generation, unit.index, unit.revision)
if key not in seen:
part = client.jobs.parse_progress_part(created.job_id, unit.content_url)
print(part.index, part.markdown)
seen.add(key)
if snapshot.status in ("succeeded", "failed", "cancelled"):
break
final = client.jobs.wait(created.job_id)Each connection lasts up to 55 seconds. Reconnect after a clean close: the first snapshot contains current state, not every past event. jobs.events() is the separate job-status stream; it does not deliver page text. Handle transport errors and an unavailable event by continuing normal job polling; closing a preview connection never cancels the job. Browser integrations should stream through their own server: native EventSource cannot attach a Bearer header, and API keys belong on the server.
Render units by their original index, even if later pages arrive first. Replace a changed revision instead of appending a duplicate; discard the previous preview when generation changes. Fetch content_url only for visible units and cache each immutable revision. Keep displayed pages and scroll position when the job completes; opening the final result should be an explicit action. Terminal snapshots have no units, so retain what was already displayed.
“All pages parsed” is followed by “Preparing final result” while tables and the full document are assembled. Only the normal job's terminal state and available result mean delivery is finished. Use job.timing.first_response_ms and job.timing.total_ms for First response and Total processing time; the response retains the timing breakdown. These are server measurements, excluding upload, browser transport/rendering and idle time waiting for another poll. Missing measurements stay unknown.
Available at every effort. Native OCR publishes pages; presentations and diagrams publish pages, and spreadsheets publish sheets after shared preparation. Some formats/providers, cache followers and Zero retention return everything together. An immediate cache hit can go straight to its final result. Streaming never guarantees a separate event for every file type. Unit previews are capped at 64K characters (truncated); snapshots at 1,000 units (more). Use the final result for complete text, exports, usage and billing.
Preview metadata and event connections count against the independent job-read per-minute window, separate from creates; individual part reads do not. Cancellation, deleted/expired sources and revoked access stop preview reads. Treat a part's 404/410 as unavailable and keep normal job polling; do not spin indefinitely on it.
Configuration
POST /v1/documents/parse has several groups of options. Each example is a whole request with that option set, for the file you uploaded above. For the SDKs and the CLI, see the Python, TypeScript and CLI pages.
Pages
Parse only some pages of the file. Leave it out to read every page.
curl https://console.nace.ai/v1/documents/parse \
-H "Authorization: Bearer $NACE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": {
"type": "workspace_file",
"workspace_id": "'"$WORKSPACE_ID"'",
"file_id": "'"$FILE_ID"'"
},
"page_ranges": [{ "start": 1, "end": 3 }]
}'| Option | Default | What it does |
|---|---|---|
page_ranges | All pages | One-based, inclusive ranges, up to 200 ranges and 500 pages in all. Leave it out to parse the whole document. |
Parse mode
How much effort parsing takes.
curl https://console.nace.ai/v1/documents/parse \
-H "Authorization: Bearer $NACE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": {
"type": "workspace_file",
"workspace_id": "'"$WORKSPACE_ID"'",
"file_id": "'"$FILE_ID"'"
},
"parse_mode": "high"
}'| Mode | Behavior |
|---|---|
low | Native OCR (default). |
medium | Routes the file to the best parser. |
high | Adds page verification and correction. Parse costs twice its standard USD price, Split and Classify cost 50% more, and Extract costs one-third more. |
Only low works with a parse_result source.
Measured latency by mode
Our production smoke measurement on 2026-11-05, through console.nace.ai, observed these times for one-page synthetic invoice PDFs:
| Mode | Parse server total | Fresh jobs |
|---|---|---|
low | 3.3 s | 1 |
medium | 3.4 s | 1 |
high | 6.2 s | 1 |
We uploaded three distinct invoices, then ran POST /v1/documents/parse?wait_seconds=60 sequentially with default output options: fixtures/t6_batch/inv_030.pdf, inv_031.pdf and inv_032.pdf, respectively, from nace-ndi-test at commit 43e5f7c. Every job succeeded with one page and a Parse cost line of execution: "fresh".
These numbers use timing.total_ms, divided by 1,000: server preparation, queueing, NDI processing and active result preparation. Upload, client network/rendering and idle time before a later read are excluded. These are individual production examples, not medians, an SLA or a per-page scaling rule. File complexity, page count, OCR, parser routing, queue load, verification and reuse affect latency. A higher mode does not guarantee a slower result on every run.
In the same five-job smoke run, Extract on fixtures/t7_odd/multipage_table.pdf (two pages, its fixture extraction schema) took 4.8 s in low and 3.1 s in high, measured as finished_at - created_at (NDI queue plus processing). Both jobs reused an earlier parse (cost.lines recorded execution: "reused" on Parse), so these are warm Extract observations and cannot establish fresh parsing latency or predict another environment's timings. Those Extract observations predate timing coverage for all document kinds and omitted timing, so their duration is not the full Drex server total used in the table above. We made no additional production jobs for this measurement.
For a fresh comparison, check cost.lines[].execution before quoting a run. Split, Classify and Extract add their own work, so Parse totals do not predict those operations' totals. Streaming can publish a usable preview before the final result; the table measures the final result, not that first preview.
Output formats
Choose which renderings of the document come back in result.document. A format you leave out comes back as null.
curl https://console.nace.ai/v1/documents/parse \
-H "Authorization: Bearer $NACE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": {
"type": "workspace_file",
"workspace_id": "'"$WORKSPACE_ID"'",
"file_id": "'"$FILE_ID"'"
},
"output": { "formats": ["markdown", "text"] }
}'| Format | Behavior |
|---|---|
markdown | The document as Markdown, for display (default). |
blocks | The layout elements, each with a location (default). |
text | The document as stripped plain text. |
Send a list, such as ["markdown", "text"]. The default is markdown and blocks.
| Option | Default | What it does |
|---|---|---|
output.include_page_markers | true | Inserts --- Page N --- between PDF pages in markdown and text. |
output.return_ocr_data | false | When true, adds word- and line-level OCR geometry on document.ocr. PDF sources only: on any other file the create is refused with 422 invalid_request. |
Table format
Controls how tables are written inside markdown and in table blocks. text always flattens tables to plain text.
curl https://console.nace.ai/v1/documents/parse \
-H "Authorization: Bearer $NACE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": {
"type": "workspace_file",
"workspace_id": "'"$WORKSPACE_ID"'",
"file_id": "'"$FILE_ID"'"
},
"output": { "table_format": "markdown" }
}'| Format | Behavior |
|---|---|
html | Tables as HTML (default). |
markdown | Tables as Markdown tables. |
Chunking
By default Parse returns no chunks. For retrieval, ask for chunks you can embed and search separately.
curl https://console.nace.ai/v1/documents/parse \
-H "Authorization: Bearer $NACE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": {
"type": "workspace_file",
"workspace_id": "'"$WORKSPACE_ID"'",
"file_id": "'"$FILE_ID"'"
},
"chunking": { "strategy": "page" }
}'| Strategy | Behavior |
|---|---|
none | chunks stays empty (default). |
page | One chunk per page. |
section | Splits at the section headings. |
Figures
Decide what happens to the figures in the document.
curl https://console.nace.ai/v1/documents/parse \
-H "Authorization: Bearer $NACE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": {
"type": "workspace_file",
"workspace_id": "'"$WORKSPACE_ID"'",
"file_id": "'"$FILE_ID"'"
},
"figures": { "mode": "describe" }
}'| Mode | Behavior |
|---|---|
include | Keeps the figure's crop (default). |
omit | Drops figures. |
describe | Keeps the crop and captions it, with one vision call per figure. |
| Option | Default | What it does |
|---|---|---|
output.include_images | false | When true, each block of type: "figure" carries a crop link in image_url, for PDF and image sources. It needs blocks in output.formats and figures.mode other than omit; a document with no detected figures gets no links. The dashboard's Include Images switch is on by default and shows each figure as an image in the results; it is unavailable while Blocks is off or figures are omitted. |
Diagrams
Decide what happens to flowchart-like figures.
curl https://console.nace.ai/v1/documents/parse \
-H "Authorization: Bearer $NACE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": {
"type": "workspace_file",
"workspace_id": "'"$WORKSPACE_ID"'",
"file_id": "'"$FILE_ID"'"
},
"diagrams": { "mode": "mermaid" }
}'| Mode | Behavior |
|---|---|
omit | Leaves them as images (default). |
mermaid | Rewrites them as Mermaid. |
Spreadsheets
For workbooks, choose the sheets to read and what to include from them.
curl https://console.nace.ai/v1/documents/parse \
-H "Authorization: Bearer $NACE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": {
"type": "workspace_file",
"workspace_id": "'"$WORKSPACE_ID"'",
"file_id": "'"$FILE_ID"'"
},
"spreadsheet": { "sheets": ["Summary"] }
}'| Option | Default | What it does |
|---|---|---|
spreadsheet.sheets | All sheets | Only these sheets, in workbook order. |
spreadsheet.include_hidden_sheets | true | When false, skips the sheets the workbook marks hidden. |
spreadsheet.include_hidden_rows | true | When false, skips the rows the workbook marks hidden. |
spreadsheet.include_hidden_columns | true | When false, skips the columns the workbook marks hidden. |
spreadsheet.include_formulas | false | When true, adds formula source text. Needs output.table_format: "html" and markdown or blocks output. Cached values are always there. |
Password
Open an encrypted PDF.
curl https://console.nace.ai/v1/documents/parse \
-H "Authorization: Bearer $NACE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": {
"type": "workspace_file",
"workspace_id": "'"$WORKSPACE_ID"'",
"file_id": "'"$FILE_ID"'"
},
"password": "..."
}'| Option | Default | What it does |
|---|---|---|
password | None | Password for an encrypted PDF. Never stored or returned. Not with a parse_result source. |
Result
A finished parse has these fields on result:
| Field | What it is |
|---|---|
result_type | Always "parse". |
file_name | The name of the parsed file. |
lane | The kind of file: pdf, document (Word), presentation, spreadsheet (workbooks, CSV and Parquet), image, html, plaintext, markdown, json, email, diagram, audio or video. |
document.markdown | The document as Markdown, for display. null when markdown isn't in output.formats. |
document.text | The document as stripped plain text. null unless text is in output.formats. |
document.blocks | The document's layout elements, each with a location. See Blocks. |
document.chunks | Pieces sized for retrieval. Empty unless chunking.strategy is page or section. See Chunks. |
document.page_count | How many pages the document has. |
ocr_applied | Whether OCR ran on the file. |
units | The job's standard-rate usage. See Usage fields. |
disclosures | Anything about how the result was made that you might not assume, such as content cut short (below), each with a code. |
{
"result_type": "parse",
"file_name": "report.pdf",
"lane": "pdf",
"document": {
"page_count": 96,
"markdown": "# Annual Report\n\n...",
"text": null,
"blocks": [ ... ],
"chunks": []
},
"ocr_applied": false,
"units": 96,
"disclosures": []
}Blocks
Each block is a layout element with a location. Its kind depends on the source:
location.kind | Where it points |
|---|---|
page_region | A one-based page and polygons in normalized 0 to 1 coordinates. Use these to draw overlays. |
sheet_range | A workbook tab and an A1 cell range. |
text_span | Character offsets into the derived text, char_start inclusive and char_end exclusive, with page when the span maps to one. |
row_range | A Parquet source: zero-based, inclusive start_row and end_row covering the file's rows. CSV files and workbooks use sheet_range. |
json_pointer | An RFC 6901 pointer into a JSON source. The empty string is the root. |
Chunks
chunking.strategy: "page" returns one page_region chunk per page. "section" follows the section headings.
Very large results
A result over 1 MiB comes back as a preview, and you fetch the full text from a link.
- Documents: when
document.markdownordocument.textwould be over 1 MiB of UTF-8, both hold a preview of at most 65,536 characters, cut at a line end. - Workbooks: when the sheets' combined rendering is over 1 MiB, every sheet gets a
content_urland a preview.
An entry in result.disclosures says the content was cut short:
{ "code": "content_truncated", "target": "document_content", "unit": "characters", "original_size": 120000000, "retained_size": 65480 }To get the full text:
- A document: fetch
document.content_url. - A workbook (
target: "spreadsheet_region"): fetch each affected sheet'sdocument.spreadsheet.sheets[].content_url.
The link answers with one of these:
| Status | What it means | What to do |
|---|---|---|
200 | The full Markdown. | Stream it to a file. |
202 | The file is still being prepared. The body is { "status": "pending", "retry_after_seconds": ... }, with a retry-after header. | Wait, then send the same request again until it answers 200. Don't save a 202 body as the document. |
409 | The content couldn't be prepared. | Asking again won't help. |
The SDKs and the CLI do this for you. jobs.download (jobs.fetchFile in TypeScript) waits for the file. nace parse fetches a document's full content itself, and for a workbook it prints a nace file JOB_ID URL command that saves each sheet.
A Parquet source (.parquet) of any size also carries rows_url, row_count and column_count on its one sheet; workbooks and CSV files never do. GET rows_url?start_row=0&limit=100 reads one page of rows, numbered from zero. limit is 1 to 100, default 50. The answer is the page:
{
"path": "...",
"start_row": 0,
"limit": 100,
"total_rows": 250000,
"columns": [{ "name": "invoice_id", "data_type": "string" }],
"rows": [{ "invoice_id": "INV-1042" }]
}Each row maps column names to the cell's text. The SDKs read a page with jobs.rows, and the CLI prints rows as TSV with nace rows JOB_ID URL.
Spreadsheets, audio and video
| File | Field | What it holds |
|---|---|---|
| Workbook | document.spreadsheet.sheets[].cell_map_url | A link to a map from the rendered table to the sheet's physical rows and columns. |
| Office file converted for layout | A converted PDF link | A link to the converted PDF. |
Audio or video (lane is "audio" or "video") | transcript_url | A link to the transcript. |
| Audio or video, when you read the job by id | result.transcript | The transcript inline: speakers, then ordered speech and silence segments with start_ms (inclusive) and end_ms (exclusive) that cover the whole recording. |
Every one of these links points at https://console.nace.ai/v1/documents/jobs/{id}/... and opens with your API key. See Jobs and results.
Errors
A parse can fail in two ways.
The server refuses the create. It answers invalid_request_error with the document service's code:
| Code | When |
|---|---|
invalid_request | An unknown body field, an option a parse_result source doesn't take, or output.return_ocr_data on a file that isn't a PDF. |
invalid_source | The source is missing, isn't a url, parse_result or workspace_file, or names a job or workspace that isn't yours. |
unsupported_file_type | Parse doesn't read this type. See Format support. |
The job is accepted and then fails. It ends with status: "failed" and a code:
| Code | When |
|---|---|
corrupt_file | The file can't be read, for example because it is damaged. |
invalid_request | A password on a file that isn't a PDF, or spreadsheet options that change a default on a file that isn't a workbook. |
Next steps
Next, pass the finished job as {"type": "parse_result", "job_id": "..."} to Extract, Split or Ground, so the document isn't parsed and paid for twice.