> This documentation belongs to Invofox, an AI-powered document processing
> platform. The API extracts structured data (fields) from unstructured
> documents (invoices, contracts, forms, bank statements, and more).
> Key concepts: Account, Environment, Document Type, Field, File, Import,
> and Document. When answering, prefer the exact terminology defined in the
> Glossary and cite the relevant API Reference endpoint when applicable.

# ZIP Files

> Upload a ZIP archive containing multiple documents in one request — each file inside is extracted and processed as an independent document.

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

<Info>
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](/documentation/classifier) to detect each type automatically.
</Info>

## 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`**

```bash title="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 `documentId`s — Invofox doesn't know how many files the ZIP contains until it's extracted. Save the `importId` instead:

**`Upload response`**

```json title="Upload response"
{
  "accountId": "000000000000000000000006",
  "environmentId": "00000000000000000000001c",
  "importId": "000000000000000000000028",   // ← save this
  "files": [
    { "id": "000000000000000000000029", "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 `documentId`s once processing is done. **Webhooks are the recommended approach**; you can also poll the import.

<Cards>
  <Card title="Option A · Webhooks (Recommended)" icon="bolt">
    Listen for `import.processed` to know when the whole ZIP is done, then fetch each document. No polling.
  </Card>
  <Card title="Option B · Poll the import" icon="arrows-rotate">
    Check the import status until processed, then collect `documentId`s from the extracted files and fetch each one.
  </Card>
</Cards>

### Option A · Webhooks (recommended for production)

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](/documentation/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 `documentId`s (the ZIP's own entry has an empty `documentIds` — skip it).

**`import.processed event`**

```json title="import.processed event"
{
  "type": "import.processed",
  "id": "000000000000000000000028",
  "timestamp": "2026-06-19T03:25:00.500Z",
  "data": {
    "id": "000000000000000000000028",
    "status": "processed",
    "files": [
      { "id": "000000000000000000000029", "filename": "batch.zip",      "status": "processed", "documentIds": [],                            "clientData": {} },
      { "id": "00000000000000000000002a", "filename": "invoice-001.pdf", "status": "processed", "documentIds": ["00000000000000000000002c"], "clientData": {} },
      { "id": "00000000000000000000002b", "filename": "invoice-002.pdf", "status": "processed", "documentIds": ["00000000000000000000002d"], "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 `documentId`s are on those child entries — iterate over all `files[]` and skip any with `compressed: true`.

<div className="ivf-endpoint"><span className="ivf-method ivf-method-get">GET</span><span>https://api.invofox.com/v1/ingest/imports/{importId}</span></div>

**`Import response (processed)`**

```json title="Import response (processed)"
{
  "id": "000000000000000000000028",
  "status": "processed",                 // ← wait for this
  "documents": 2,                        // ← total documents extracted (reliable once processed)
  "files": [
    {
      "id": "000000000000000000000029",
      "filename": "batch.zip",
      "status": "processed",
      "compressed": true,                // ← the ZIP itself
      "documentIds": []                  // ← skip this entry
    },
    {
      "id": "00000000000000000000002a",
      "filename": "invoice-001.pdf",     // ← keeps its original name inside the ZIP
      "status": "processed",
      "compressed": false,
      "parentFileId": "000000000000000000000029",   // ← points to the ZIP entry
      "documentIds": ["00000000000000000000002c"]   // ← fetch this
    },
    {
      "id": "00000000000000000000002b",
      "filename": "invoice-002.pdf",
      "status": "processed",
      "compressed": false,
      "parentFileId": "000000000000000000000029",
      "documentIds": ["00000000000000000000002d"]
    }
  ]
}
```

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`**

```json title="Document from a ZIP"
{
  "result": {
    "_id": "00000000000000000000002c",
    "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":  "000000000000000000000028", // ← importId
      "file": "00000000000000000000002a", // ← fileId of the extracted file
      "fromZip": true,                     // ← marks a document that came from a ZIP
      "filename": "invoice-001.pdf"
    }
  }
}
```

<Warning>
**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](/documentation/rejected-files) for how to handle the `file.rejected` webhook.
</Warning>