Create a document from a file

A document is the unit Circularo manages: a main file plus a title, structured data, a place in your folders, and an evidence trail. This guide creates one from a file you uploaded, creates a second one with a category and an attachment, and reads a document back. It takes three calls.

Before you begin

  • An uploaded file. The document is built on a file identifier — Upload and download files shows how to obtain one.

  • Attaching files needs the add_file_attachments right. Without it, a create call that carries attachments is rejected with 403; creating documents without them is unaffected.

Step 1: Create the document

A title and the file are all a document needs:

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

{
    "title": "Employment Agreement (Jane Doe)",
    "mainFileId": "4b8e17d0c92a35f6e1478bc0d25a9e31"
}
HTTP/2 201

{
    "annotations": [],
    "attachments": [],
    "category": null,
    "createdAt": "2026-05-11T09:30:00.000Z",
    "createdBy": "john.smith@example.com",
    "customFields": [],
    "folders": [
        {
            "folderId": null,
            "scope": "personal"
        }
    ],
    "id": "pXq4NcYBhK9tWdA2mFzR",
    "images": [],
    "mainFile": {
        "fileId": "4b8e17d0c92a35f6e1478bc0d25a9e31",
        "fileName": "Employment Agreement (Jane Doe)",
        "mimeType": "application/pdf",
        "pageCount": 1,
        "size": 1852
    },
    "metadataDefinition": "d_default",
    "metadataFields": {},
    "owner": "john.smith@example.com",
    "signatureProvider": null,
    "signatures": [],
    "submittedFiles": [],
    "templateId": null,
    "title": "Employment Agreement (Jane Doe)",
    "transaction": {
        "id": "Vt7RfKq2LmXe5ZwGyB4N",
        "status": "not-started",
        "waitingFor": []
    },
    "trashed": false,
    "webUrl": "https://sandbox.circularo.com/home/detail?select=pXq4NcYBhK9tWdA2mFzR"
}

A few things to notice:

  • The document renames its main file after itself: mainFile.fileName matches title, not the name the file was uploaded with.

  • metadataDefinition was not given, so the document was created with d_default. The definition decides which structured fields the document can carry — Store structured data on a document covers them.

  • folders places the document at the root of your personal tree, and transaction is not-started: nothing has been sent to anyone yet.

  • webUrl is a ready-made link to the document in the Circularo web app, for people who have an account there.

Keep the id. Every later call — sending the document, reading it, downloading its file — addresses the document by it.

Step 2: Create a document with more than the minimum

The same call accepts more than a title and a file. This one creates a second document from a separately uploaded agreement, and gives it a category and a supporting file alongside the main one:

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

{
    "title": "Employment Agreement (Robert Brown)",
    "category": "Contract",
    "mainFileId": "7a1c58e04f36b9d2c8e70a45db19f6e3",
    "attachments": [
        {
            "fileId": "c05a9b73e6142f8dab37e9105c4d268f",
            "fileName": "Benefits summary",
            "isPublic": true
        }
    ]
}
HTTP/2 201

{
    "id": "hL8vXe3RmQ7zBnT5wKdY",
    "title": "Employment Agreement (Robert Brown)",
    "category": "Contract",
    "mainFile": {
        "fileId": "7a1c58e04f36b9d2c8e70a45db19f6e3",
        "fileName": "Employment Agreement (Robert Brown)",
        "mimeType": "application/pdf",
        "pageCount": 1,
        "size": 1852
    },
    "attachments": [
        {
            "fileId": "c05a9b73e6142f8dab37e9105c4d268f",
            "fileName": "Benefits summary",
            "isMerged": false,
            "isPublic": true,
            "mimeType": "application/pdf",
            "pageCount": 3,
            "size": 818513
        }
    ],
    ...
}

A few things to notice:

  • Each attachment references a file you have already uploaded. fileName is how the attachment is presented, and isPublic decides whether external recipients of the document may see it.

  • Attachments are supplied while the document is being created, together with everything else it is made of.

  • category groups documents by a label your workspace has configured. GET /me returns the available ones in settings.documentCategories.

Tip

The main file is kept exactly as it was uploaded, unless you ask for something else: the optional pdfConversion parameter has the file rendered while the document is created — to PDF, or to PDF/A for long-term archiving. The rendered file then becomes the document's main file, so mainFile.fileId is a new identifier rather than the one you referenced.

Step 3: Read the document back

A document is retrieved by its identifier, and the response is the same representation the create call returned:

GET /documents/pXq4NcYBhK9tWdA2mFzR
Authorization: Bearer YOUR_API_KEY
HTTP/2 200

{
    "id": "pXq4NcYBhK9tWdA2mFzR",
    "title": "Employment Agreement (Jane Doe)",
    "folders": [
        {
            "folderId": null,
            "scope": "personal"
        }
    ],
    "transaction": {
        "id": "Vt7RfKq2LmXe5ZwGyB4N",
        "status": "not-started",
        "waitingFor": []
    },
    "trashed": false,
    "webUrl": "https://sandbox.circularo.com/home/detail?select=pXq4NcYBhK9tWdA2mFzR",
    ...
}

Three fields describe where the document stands: transaction.status is not-started until you send it to someone, folders lists the folder trees it sits in, and trashed tells you whether it has been moved to the trash.

Complete example

Uploading two files and creating a document with an attachment, 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 upload = async (name, path) => {
    const form = new FormData();
    form.set("fileName", name);
    form.set("file", new Blob([await fs.readFile(path)]), path);
    const res = await fetch(`${BASE_URL}/files`, { method: "POST", headers: AUTH_HEADER, body: form });
    if (!res.ok) throw new Error(`Upload failed: ${res.status}`); // error handling kept minimal for brevity
    return (await res.json()).id;
};

const mainFileId = await upload("Employment Agreement", "employment-agreement.pdf");
const benefitsFileId = await upload("Benefits summary", "benefits-summary.pdf");

const createRes = await fetch(`${BASE_URL}/documents`, {
    method: "POST",
    headers: { ...AUTH_HEADER, "Content-Type": "application/json" },
    body: JSON.stringify({
        title: "Employment Agreement (Robert Brown)",
        category: "Contract",
        mainFileId: mainFileId,
        attachments: [
            {
                fileId: benefitsFileId,
                fileName: "Benefits summary",
                isPublic: true
            }
        ]
    })
});
if (!createRes.ok) throw new Error(`Create failed: ${createRes.status}`);

const document = await createRes.json();
console.log(`Document ${document.id} created with ${document.attachments.length} attachment(s)`);

Next steps