Export documents as archives

An export packs documents and the evidence around them into a ZIP archive. Which way it arrives depends on the endpoint you call: the one that exports a single document answers with the archive itself, while every other export is prepared in the background and collected once it is ready. This guide does both, in four calls.

Before you begin

  • Documents to export. The examples export employment agreements filed under the Contract category.

Step 1: Export a single document

The request body says what goes into the archive:

POST /documents/pXq4NcYBhK9tWdA2mFzR/export
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

{
    "includeAuditTrail": true,
    "includeAttachments": true
}
HTTP/2 200
Content-Type: application/zip

The archive arrives in the response itself — no job, no polling. Alongside the audit trail and the attachments shown here, the body can ask for the certificate of fulfillment and the files recipients submitted, merge everything into one PDF, seal the result, or protect it with a password; the API reference lists them all.

Every export is a ZIP, even this one covering a single document — an export is the document's package, not a bare file. To download just the document's own PDF, use the file endpoint instead, as in Upload and download files.

Step 2: Queue an export of a selection

Every export other than the single-document one runs in the background, however much it ends up containing. This one takes the filters of a document search, plus the same archive options:

POST /documents/export
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

{
    "category": [
        "Contract"
    ],
    "includeAuditTrail": true
}
HTTP/2 202

{
    "fileId": null,
    "finishedAt": null,
    "id": "f0a41d7c-92b5-4c86-9f3d-58ac21e7b604",
    "isEmpty": false,
    "queuedAt": "2026-05-11T09:30:00.000Z",
    "startedAt": null,
    "status": "queued",
    "type": "documents"
}

The response is the job, not the archive: 202 means the work has been accepted. status is still queued, and fileId and finishedAt are null because nothing has been produced yet. Keep the id.

Step 3: Poll until the export is finished

Read the job until it reaches a terminal status:

GET /exports/f0a41d7c-92b5-4c86-9f3d-58ac21e7b604
Authorization: Bearer YOUR_API_KEY
HTTP/2 200

{
    "fileId": "9c31f7e0b58a2d64c07be9152a83df41",
    "finishedAt": "2026-05-11T09:34:00.000Z",
    "id": "f0a41d7c-92b5-4c86-9f3d-58ac21e7b604",
    "isEmpty": false,
    "queuedAt": "2026-05-11T09:30:00.000Z",
    "startedAt": "2026-05-11T09:32:00.000Z",
    "status": "completed",
    "type": "documents"
}

A few things to notice:

  • status walks queuedprocessingcompleted, or ends in failed when the archive could not be produced.

  • fileId appears once the export completed — that is the finished archive.

  • isEmpty tells you whether the archive has any documents in it, which matters for an export that ran on a filter rather than on a selection you already know.

Step 4: Download the archive

The result is an ordinary file, so it is downloaded like any other:

GET /files/9c31f7e0b58a2d64c07be9152a83df41/content
Authorization: Bearer YOUR_API_KEY
HTTP/2 200
Content-Type: application/zip

An export job stays readable after it finished, so the archive can be collected later — and only by the account that started the export; anybody else gets a 404.

Two more exports work exactly the same way, differing only in what they gather: POST /folders/export takes a whole folder tree by its scope, or the folders you name in folderIds, while POST /me/export packs up your own documents together with their metadata. Both answer with the same job, polled and downloaded as above.

Note

When the selection matches nothing, no job is created and the answer is 404. A scheduled export that finds nothing in its window is therefore an expected outcome, not a failure — treat that status as "nothing to export this time".

Because a queued export is a create, it accepts an Idempotency-Key, so a retried request cannot start the same export twice — Retry requests safely covers that.

Complete example

Queueing an export, waiting for it, and saving the archive, as a Node.js script. Error handling is minimal for brevity — in production, inspect the error envelope of non-2xx responses.

JavaScript
import fs from "node:fs/promises";

const BASE_URL = "https://sandbox.circularo.com/api/v1/public"; // your instance's base URL
const API_KEY = "YOUR_API_KEY";
const AUTH_HEADER = { "Authorization": `Bearer ${API_KEY}` };

const startRes = await fetch(`${BASE_URL}/documents/export`, {
    method: "POST",
    headers: { ...AUTH_HEADER, "Content-Type": "application/json" },
    body: JSON.stringify({
        category: [
            "Contract"
        ],
        includeAuditTrail: true
    })
});
if (startRes.status === 404) throw new Error("Nothing matched the filter"); // error handling kept minimal for brevity

let job = await startRes.json();
while (job.status === "queued" || job.status === "processing") {
    await new Promise((resolve) => setTimeout(resolve, 5_000));
    job = await (await fetch(`${BASE_URL}/exports/${job.id}`, { headers: AUTH_HEADER })).json();
}
if (job.status !== "completed") throw new Error(`Export ${job.id} ${job.status}`);

const archive = await fetch(`${BASE_URL}/files/${job.fileId}/content`, { headers: AUTH_HEADER });
await fs.writeFile("export.zip", Buffer.from(await archive.arrayBuffer()));

Next steps