ZIP Files

Upload a ZIP archive of many documents in a single call.

Upload a ZIP archive containing multiple documents in a single call. Each file inside the ZIP is extracted and processed as an independent document.

Each file inside the ZIP becomes a separate document — they can be of different types. Bundle a batch from an external system into one ZIP and send a single request. You can also combine a ZIP upload with the Classifier to detect each type automatically.

Step 1 — Upload the ZIP

Upload the ZIP exactly like any other file — same endpoint, same info field. Invofox detects the MIME type automatically.

cURL
curl -X POST https://api.invofox.com/v1/ingest/uploads \
-H "x-api-key: $INVOFOX_API_KEY" \
-F "files=@batch.zip" \
-F 'info={"type":"<TYPE_ID>"}'

The upload response does not include documentIds — Invofox doesn’t know how many files the ZIP contains until it’s extracted. Save the importId instead:

Upload response
{
"accountId": "666839c78ea472012a9b8875",
"environmentId": "6a338e5239207f9311eccd36",
"importId": "6a34b66df55a9cdc5e6d1ef0", // ← save this
"files": [
{ "id": "6a34b66df55a9cdc5e6d1ef3", "filename": "batch.zip" }
]
// no documentId — that's expected on a ZIP upload
}

Any clientData you include in the info field is propagated to every document extracted from the ZIP — all of them share the exact same clientData.

Step 2 — Get the results

Retrieve the documentIds once processing is done. Webhooks are the recommended approach; you can also poll the import.

Option A · Webhooks (Recommended)

Listen for import.processed to know when the whole ZIP is done, then fetch each document. No polling.

Option B · Poll the import

Check the import status until processed, then collect documentIds from the extracted files and fetch each one.

A document.processed event fires for each file as it finishes, carrying the full extracted document in its data field — same payload format as Step 3 of Integrating Invofox.

Because you don’t know upfront how many files a ZIP contains, you can’t rely on counting document.processed events to know when the ZIP is fully done. Instead, listen for import.processed — this event fires once, after every file in the ZIP has been processed. Iterate data.files[] for the documentIds (the ZIP’s own entry has an empty documentIds — skip it).

import.processed event
{
"type": "import.processed",
"id": "6a34b66df55a9cdc5e6d1ef0",
"timestamp": "2026-06-19T03:25:00.500Z",
"data": {
"id": "6a34b66df55a9cdc5e6d1ef0",
"status": "processed",
"files": [
{ "id": "6a34b66df55a9cdc5e6d1ef3", "filename": "batch.zip", "status": "processed", "documentIds": [], "clientData": {} },
{ "id": "6a34b66e6e5e80ef22435412", "filename": "invoice-001.pdf", "status": "processed", "documentIds": ["6a34b68afb139487f5bcaf92"], "clientData": {} },
{ "id": "6a34b66e6e5e80ef2243541d", "filename": "invoice-002.pdf", "status": "processed", "documentIds": ["6a34b68bfb139487f5bcafb7"], "clientData": {} }
]
}
}

Option B · Poll the import

Poll GET /v1/ingest/imports/{importId} until status is "processed". While processing, status is "processing" and the documents counter increases as each file is extracted — don’t rely on it until status is "processed". The response holds one entry for the ZIP itself (compressed: true, documentIds: []) plus one entry per extracted file. The real documentIds are on those child entries — iterate over all files[] and skip any with compressed: true.

Import response (processed)
{
"id": "6a34b66df55a9cdc5e6d1ef0",
"status": "processed", // ← wait for this
"documents": 2, // ← total documents extracted (reliable once processed)
"files": [
{
"id": "6a34b66df55a9cdc5e6d1ef3",
"filename": "batch.zip",
"status": "processed",
"compressed": true, // ← the ZIP itself
"documentIds": [] // ← skip this entry
},
{
"id": "6a34b66e6e5e80ef22435412",
"filename": "invoice-001.pdf", // ← keeps its original name inside the ZIP
"status": "processed",
"compressed": false,
"parentFileId": "6a34b66df55a9cdc5e6d1ef3", // ← points to the ZIP entry
"documentIds": ["6a34b68afb139487f5bcaf92"] // ← fetch this
},
{
"id": "6a34b66e6e5e80ef2243541d",
"filename": "invoice-002.pdf",
"status": "processed",
"compressed": false,
"parentFileId": "6a34b66df55a9cdc5e6d1ef3",
"documentIds": ["6a34b68bfb139487f5bcafb7"]
}
]
}

Each extracted file keeps its original name inside the archive (files[].filename) — unlike the Splitter, which renames its outputs to split_n.pdf. Collect every documentId across the child entries.

Fetch each document

For each documentId, call GET /documents/{documentId} — same response format as the standard flow. Documents extracted from a ZIP carry import.fromZip: true and import.filename set to the file’s name inside the archive:

Document from a ZIP
{
"result": {
"_id": "6a34b68afb139487f5bcaf92",
"type": "<document type id or alias>",
"name": "invoice-001.pdf", // ← keeps its name inside the ZIP
"publicState": "approved",
"confidence": "high",
"data": { "...": "extracted fields" },
"import": {
"ref": "6a34b66df55a9cdc5e6d1ef0", // ← importId
"file": "6a34b66e6e5e80ef22435412", // ← fileId of the extracted file
"fromZip": true, // ← marks a document that came from a ZIP
"filename": "invoice-001.pdf"
}
}
}

A file inside the ZIP can fail on its own (unsupported format, corrupt, or password-protected) while the import as a whole still reaches status: "processed" — so always check each child’s status/error. See Rejected files for how to handle the file.rejected webhook.