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:
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:
-
fileNameis the name Circularo stores, without an extension. Omit it and the uploaded file's own name is used, with its extension dropped. -
checksumis the SHA-512 hash of the stored content. Hash your local file and compare the two to be certain the upload arrived intact. -
anchorPositionsis 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.
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.
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.
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
-
Turn the file into a document Circularo manages — Create a document from a file.
-
Send it for signature straight away — Signing & Approvals.