Upload
There are two ways to upload documents: the POST /v1/documents shortcut
for a single file, and POST /v1/batches for multiple files. Both paths
create the same underlying Batch/Document records; POST /v1/documents
opens a single-item batch behind the scenes. id values are always
returned immediately -- the client gets document.id (and batch.id)
without waiting for parsing to finish, and the next step is polling
GET /v1/jobs/{id} or a webhook (see Polling, Webhook).
Auth and entitlement
All upload requests require Authorization: Bearer <api_key>
(dip_live_... or dip_test_..., see Getting Started and Auth). Upload
triggers the extract module -- if extract is not in the key's
entitlements list, the request is rejected with 403 service_not_enabled
without the file ever being read. This check applies to test keys as well:
TEST-KEY-EXEMPTION only covers billing and rate-limit, not access.
POST /v1/documents (single upload)
multipart/form-data body.
| Field | Required | Description |
|---|---|---|
file | yes | The file to upload |
external_item_ref | no | Document reference in your own system |
external_batch_ref | no | Reference for the auto-created single-item batch behind the scenes |
callback_url | no | If left blank, the tenant's default webhook address is used |
The response (201 Created) is a single document object with the
same schema as GET /v1/jobs/{id}, with status: "queued": id has
an opaque d_ prefix, batch_id (the id of the auto-created single-item
batch) has an opaque b_ prefix. result and error are still null,
and page_count is not yet known either (it is filled in after the split
step). See Polling for the full field list.
curl example and full response body in the right panel.
POST /v1/batches (multi upload)
One or more files are uploaded in a single request with a
multipart/form-data body.
| Field | Required | Description |
|---|---|---|
metadata | yes | JSON string part (below) |
item_0, item_1, ... | yes | File parts matching metadata.items[].key |
metadata body:
{
"external_batch_ref": "PO-2026-001",
"callback_url": "https://tenant.example.com/webhooks/docsfra",
"items": [
{ "key": "item_0", "external_item_ref": "INV-001" },
{ "key": "item_1", "external_item_ref": "INV-002" }
]
}
external_batch_ref,callback_url-- optional (see above).items[]-- each entry containskey(the name of the corresponding file part) and an optionalexternal_item_ref. For eachitems[].keya file part with the same name (item_0,item_1, ...) must be sent in the request -- if no matching file is found,400 invalid_request.
The response (201 Created) is a batch object: id (b_ prefix),
external_batch_ref, status (derived from documents[]: completed/
failed if all are completed/failed, processing otherwise, queued
if none have been processed yet), created_at, documents[] -- each a
short representation (id, external_item_ref, status). Use
GET /v1/batches/{id} for detailed status (see Polling).
curl example and full response body in the right panel.
Supported formats and limits
At upload time both the Content-Type and the signature in the file's
first bytes (magic number) are validated -- relying on the extension/header
alone can be spoofed, so both are checked together.
| Format | Content-Type |
|---|---|
application/pdf | |
| DOCX / DOC | application/vnd.openxmlformats-officedocument.wordprocessingml.document / application/msword |
| XLSX / XLS | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet / application/vnd.ms-excel |
| PPTX / PPT | application/vnd.openxmlformats-officedocument.presentationml.presentation / application/vnd.ms-powerpoint |
| ODT / ODS / ODP | application/vnd.oasis.opendocument.text / .spreadsheet / .presentation |
| RTF | application/rtf |
| TXT / HTML / CSV / Markdown | text/plain, text/html, text/csv, text/markdown |
| PNG | image/png |
| JPEG / JPG | image/jpeg |
| TIFF | image/tiff |
| Email (.eml) | message/rfc822 |
| Audio (mp3/wav/m4a/ogg/webm/opus/amr) | audio/mpeg, audio/wav / audio/x-wav, audio/mp4 / audio/m4a, audio/ogg, audio/webm, audio/opus, audio/amr |
| Video (mp4/mov/avi/mkv/webm) | video/mp4, video/quicktime, video/x-msvideo, video/x-matroska, video/webm |
- Office/text formats (everything except PDF/image/email/audio above) are converted to PDF on our own infrastructure (LibreOffice, no third-party cloud) before the same page-render pipeline processes them -- the output is identical to uploading an equivalent PDF.
- Legacy/alternative
Content-Typevalues (e.g. a client sending an actual.docxfile with the olderapplication/mswordheader) are resolved automatically by checking the file's real signature against both the declared header and the extension -- whichever one matches wins. - If
Content-Typeis empty orapplication/octet-stream, it is guessed from the file extension; it must still pass the magic-number check -- name/extension alone is not sufficient. - If even one file in a request does not match the whitelist, or its
magic number does not match the declared format, the entire request is
rejected (
422 unprocessable_file) -- there is no partial acceptance; you must filter out the valid files and resubmit. - If the file size exceeds the server-side upper limit (default 1 GB,
configurable),
413 payload_too_largeis returned. - Email (.eml,
message/rfc822) uploads are parsed deterministically into canonical markdown: subject and headers (From/To/Cc/Date), body, and an attachment list (name · type · size) -- attachments themselves are not processed separately in v1. - Audio (mp3/wav/m4a/ogg/webm/opus/amr — WhatsApp voice notes included)
uploads are automatically transcribed
into markdown with
[MM:SS]timestamped lines. Video (mp4/mov/avi/mkv/webm) goes through the same pipeline: the audio track is extracted and transcribed — frames/visual content are not analyzed in v1. Audio support must be configured on the platform (AUDIO_TRANSCRIBE_API_URL) -- if it isn't, audio uploads are rejected with400 unsupported_media.
What happens after upload
A 201 response does not mean the job is done -- it only means the
document has been queued. All of the steps below proceed automatically
on the server; you don't need to make any additional calls:
queued -> processing-- the file is split into page images (page-by-page resilient pipeline).parsing-- each page is parsed separately. If a page is permanently exhausted across all cloud engines, that page is skipped but the pipeline does not stop; the other pages are not lost.indexing-- pages are merged inpage_noorder to produce canonical markdown + JSON; a best-effort structuring (structuring) is also attempted.completed--result.markdown_url/result.json_url(andresult.structured_urlif it could be produced) become available. Ifpartial: true, at least one page was lost in step 2; the document is still consideredcompleted, and the remaining pages are not lost. At this point credit is deducted: theextractmodule (per page) is always deducted, while thestructuremodule (per page) is deducted separately only if structuring succeeded.- AI indexing (proceeds in the background, independent of
completed) -- the document is split into chunks, embedded, and written to the vector index (embedandindexmodules, billed separately). Once complete, the document becomes queryable via Semantic Search and Q&A.
Track progress by polling GET /v1/jobs/{id} or by listening for a
webhook (see Polling, Webhook) -- both converge to the same final state.
Uploading with a test key
Uploads made with a dip_test_-prefixed key go through the exact same
pipeline -- parsing, structuring, and AI indexing all work normally. The
difference is only in billing:
- The pre-upload balance check (
BILL-PRECHECK) is skipped -- you will not get402 insufficient_creditwith test keys. - Usage for
extract/structure/embed/indexis still measured (aUsageEventis written as an audit trail), but it is not deducted from the tenant balance. - Test keys are exempt from the
tenant_uploadthrottle; instead they are subject to a fixed, generous hard cap (default1000/day, per key rather than per tenant).
The entitlement check (403 service_not_enabled) applies to test keys as
well -- the exemption only covers billing and rate-limit.
Errors
| HTTP | code | Description |
|---|---|---|
| 400 | invalid_request | metadata is invalid JSON / items is empty / no file part matches a key / file is missing |
| 401 | unauthorized | missing, invalid, or revoked key |
| 402 | insufficient_credit | balance insufficient in the pre-check using a rough file-size estimate (test keys exempt) |
| 403 | service_not_enabled | extract is not in the key's entitlements list |
| 413 | payload_too_large | file size exceeded the limit |
| 422 | unprocessable_file | a file format is not supported -- the entire request is rejected, there is no partial acceptance |
| 400 | unsupported_media | an audio file was uploaded but AUDIO_TRANSCRIBE_API_URL is not configured on the platform |
| 429 | rate_limited | the tenant_upload throttle scope (60/min) was exceeded |
| 503 | service_unavailable | temporarily unable to write to media-cdn |