Docsfra
API Documentation
v1

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.

FieldRequiredDescription
fileyesThe file to upload
external_item_refnoDocument reference in your own system
external_batch_refnoReference for the auto-created single-item batch behind the scenes
callback_urlnoIf 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.

FieldRequiredDescription
metadatayesJSON string part (below)
item_0, item_1, ...yesFile 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 contains key (the name of the corresponding file part) and an optional external_item_ref. For each items[].key a 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.

FormatContent-Type
PDFapplication/pdf
DOCX / DOCapplication/vnd.openxmlformats-officedocument.wordprocessingml.document / application/msword
XLSX / XLSapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet / application/vnd.ms-excel
PPTX / PPTapplication/vnd.openxmlformats-officedocument.presentationml.presentation / application/vnd.ms-powerpoint
ODT / ODS / ODPapplication/vnd.oasis.opendocument.text / .spreadsheet / .presentation
RTFapplication/rtf
TXT / HTML / CSV / Markdowntext/plain, text/html, text/csv, text/markdown
PNGimage/png
JPEG / JPGimage/jpeg
TIFFimage/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-Type values (e.g. a client sending an actual .docx file with the older application/msword header) are resolved automatically by checking the file's real signature against both the declared header and the extension -- whichever one matches wins.
  • If Content-Type is empty or application/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_large is 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 with 400 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:

  1. queued -> processing -- the file is split into page images (page-by-page resilient pipeline).
  2. 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.
  3. indexing -- pages are merged in page_no order to produce canonical markdown + JSON; a best-effort structuring (structuring) is also attempted.
  4. completed -- result.markdown_url / result.json_url (and result.structured_url if it could be produced) become available. If partial: true, at least one page was lost in step 2; the document is still considered completed, and the remaining pages are not lost. At this point credit is deducted: the extract module (per page) is always deducted, while the structure module (per page) is deducted separately only if structuring succeeded.
  5. AI indexing (proceeds in the background, independent of completed) -- the document is split into chunks, embedded, and written to the vector index (embed and index modules, 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 get 402 insufficient_credit with test keys.
  • Usage for extract/structure/embed/index is still measured (a UsageEvent is written as an audit trail), but it is not deducted from the tenant balance.
  • Test keys are exempt from the tenant_upload throttle; instead they are subject to a fixed, generous hard cap (default 1000/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

HTTPcodeDescription
400invalid_requestmetadata is invalid JSON / items is empty / no file part matches a key / file is missing
401unauthorizedmissing, invalid, or revoked key
402insufficient_creditbalance insufficient in the pre-check using a rough file-size estimate (test keys exempt)
403service_not_enabledextract is not in the key's entitlements list
413payload_too_largefile size exceeded the limit
422unprocessable_filea file format is not supported -- the entire request is rejected, there is no partial acceptance
400unsupported_mediaan audio file was uploaded but AUDIO_TRANSCRIBE_API_URL is not configured on the platform
429rate_limitedthe tenant_upload throttle scope (60/min) was exceeded
503service_unavailabletemporarily unable to write to media-cdn