Upload and download files

Every document Circularo manages is built on a file, and every signed result comes back as one. This guide uploads a PDF, looks its metadata up again, and downloads its content — three calls that cover the whole file surface.

Before you begin

  • A file to upload. Any file works; the examples use a one-page PDF named employment-agreement.pdf.

Step 1: Upload the file

A file is stored on its own and takes on a role only once something references it — as a document's main file, as an attachment, or as the image applied when signing. Send the content as multipart/form-data, in the file part:

POST /files
Authorization: Bearer YOUR_API_KEY
Content-Type: multipart/form-data

fileName: Employment Agreement
file: @employment-agreement.pdf (file content)

The same upload with curl:

Bash
curl -X POST "https://sandbox.circularo.com/api/v1/public/files" \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -F "fileName=Employment Agreement" \
    -F "file=@employment-agreement.pdf"
HTTP/2 201

{
    "checksum": "cebba9cbff080cb68b02837a951b4e6ab07b79205d7b940b1a4b8ceb00f11add4c5401a705483a6b8aea975a8ce3c2044dcd8912ea67516700901f7d0a1af44d",
    "createdAt": "2026-05-11T09:30:00.000Z",
    "createdBy": "john.smith@example.com",
    "fileName": "Employment Agreement",
    "hasPassword": false,
    "id": "4b8e17d0c92a35f6e1478bc0d25a9e31",
    "mimeType": "application/pdf",
    "pageCount": 1,
    "size": 1852,
    "anchorPositions": []
}

A few things to notice:

  • fileName is the name Circularo stores, without an extension. Omit it and the uploaded file's own name is used, with its extension dropped.

  • checksum is the SHA-512 hash of the stored content. Hash your local file and compare the two to be certain the upload arrived intact.

  • anchorPositions is empty because this upload asked for nothing to be located in the document.

Keep the id. It is the value later requests use to point at this file, wherever one is expected.

Note

You do not have to upload separately. POST /transactions also accepts the content inline and sends a document in a single call — see Quickstart: send your first document for signature. Upload first when you want to reuse the same file for several documents, or when you want the file identifier before you decide what to do with it.

Tip

When the file is a PDF, the upload can also locate text in it and return the positions where signature fields belong, ready to be used as placed fields — see Position fields with anchor text.

Step 2: Look up the file later

The file's metadata is read back by its identifier:

GET /files/4b8e17d0c92a35f6e1478bc0d25a9e31
Authorization: Bearer YOUR_API_KEY
HTTP/2 200

{
    "id": "4b8e17d0c92a35f6e1478bc0d25a9e31",
    "fileName": "Employment Agreement",
    "mimeType": "application/pdf",
    "size": 1852,
    ...
}

This is the same metadata the upload returned, without the anchor positions — those belong to the upload alone.

Step 3: Download the content

The content sub-resource streams the bytes themselves:

GET /files/4b8e17d0c92a35f6e1478bc0d25a9e31/content
Authorization: Bearer YOUR_API_KEY
HTTP/2 200
Content-Type: application/pdf
Content-Disposition: attachment; filename="Employment Agreement.pdf"; filename*=UTF-8''Employment%20Agreement.pdf

The body is the file, not JSON: the response carries the file's own media type and a Content-Disposition header naming it, so a browser or HTTP client treats it as a download. Read it as a byte stream and write it wherever your process keeps documents.

Complete example

Uploading a file, verifying it arrived intact, and downloading it again, as a Node.js script. Error handling is minimal for brevity — in production, inspect the error envelope of non-2xx responses.

JavaScript
import crypto from "node:crypto";
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 content = await fs.readFile("employment-agreement.pdf");

const form = new FormData();
form.set("fileName", "Employment Agreement");
form.set("file", new Blob([content]), "employment-agreement.pdf");

const uploadRes = await fetch(`${BASE_URL}/files`, { method: "POST", headers: AUTH_HEADER, body: form });
if (!uploadRes.ok) throw new Error(`Upload failed: ${uploadRes.status}`); // error handling kept minimal for brevity

const file = await uploadRes.json();
const localChecksum = crypto.createHash("sha512").update(content).digest("hex");
if (file.checksum !== localChecksum) throw new Error("The stored file does not match the file that was sent");

const download = await fetch(`${BASE_URL}/files/${file.id}/content`, { headers: AUTH_HEADER });
await fs.writeFile("downloaded.pdf", Buffer.from(await download.arrayBuffer()));

Next steps