Multi-page documents
Updated
When this applies#
A PDF of more than 3 pages is not processed synchronously. documents splits it and returns 202 instead of 200.
This is automatic and cannot be disabled. Any integration that accepts arbitrary PDFs must handle the 202 branch — otherwise it will appear to work in testing on small files and fail in production on real documents.
Images are always single-page and always synchronous.
Limits#
| Limit | Value |
|---|---|
| Sync threshold | 3 pages |
| Maximum pages | 150 |
| Maximum file size | 10 MB |
| Job TTL | 2 hours from split |
| Open jobs per key | 5 |
| Chunk size | 3 pages |
The three calls#
1. Upload — returns a manifest#
curl -X POST https://api.docxtract.io/v3.1/documents \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@50-page.pdf" \
-F 'options={"model":"invoice"}'
{
"success": true,
"data": [
{ "job_id": "a1b2…", "pages": "1-3" },
{ "job_id": "c3d4…", "pages": "4-6" }
],
"job_id": "parent99…",
"pages": 50,
"expires_at": "2026-08-23T12:00:00Z"
}
data is the chunk list. The root job_id is the parent — you need both.
result_url field. It has been emitting a path
that 404s. Ignore it and build the URL yourself:
https://api.docxtract.io/v3.1/result?job_id={parent job_id}.2. Process each chunk#
Once per chunk job_id, sequentially or in parallel:
curl -X POST "https://api.docxtract.io/v3.1/process?job_id=a1b2…" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F 'options={"model":"invoice"}'
The model is chosen here, per chunk — the split itself is model-independent.
{ "success": true, "data": null, "job_id": "a1b2…", "status": "done",
"code": "chunk_processed", "pages": "1-3", "chunks_done": 1, "chunks_total": 17 }
data is always null; content comes from step 3. A code of chunk_already_processed means the chunk was already done and the call replayed harmlessly.
3. Collect the result#
curl "https://api.docxtract.io/v3.1/result?job_id=parent99…" \
-H "Authorization: Bearer YOUR_API_KEY"
{
"success": true,
"data": { },
"job_id": "parent99…",
"status": "complete",
"pages_total": 50,
"pages_processed": 50,
"chunks_done": 17,
"chunks_total": 17,
"credits_used": 50
}
status is complete or partial. A partial response adds pending_pages and failed_pages, which makes this endpoint usable as a progress poll — it is a pure read and re-fetchable any number of times within the TTL.
Finalizing#
result?job_id=…&finalize=true permanently deletes all extracted data for the job after responding.
Without finalize, data is cleaned up automatically when the 2-hour TTL expires.
Mixing models across chunks#
Permitted, but the collect response then carries a mixed_models warning and the combined result may be internally inconsistent. Use one model per document unless you have a specific reason.
Recommended flow#
POST documents.php— inspect the status code, not just the body- On
200, you are done - On
202, keep the parentjob_idand the chunk list - Call
processper chunk with bounded parallelism; retry failures with backoff - Call
resultwith the parentjob_id - If
statusispartial, retry the chunks named infailed_pages - Store the result, then optionally
finalize=true
Watch the clock throughout — everything must complete inside the 2-hour TTL, after which chunks are unrecoverable and the document must be re-uploaded.
Official SDKs (Phase 2 onward) collapse all of this into a single extract() call.
Something wrong or missing on this page? Tell us.