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
Contractcategory.
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:
-
statuswalksqueued→processing→completed, or ends infailedwhen the archive could not be produced. -
fileIdappears once the export completed — that is the finished archive. -
isEmptytells 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.
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.
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
-
Choose exactly which documents an export covers — Search documents.
-
Collect the evidence of a single document without an archive — Collect the evidence of a signed document.