Place fields on the document

The quickstart placed a single signature box. Real documents need more: somewhere to sign, a form field the signer fills in, a fixed label, a slot for an image. This guide places one field of each kind, follows them while the signer works, and reads back what they entered. It takes three calls.

Before you begin

Where fields sit on the page

Every field carries pages — the pages it appears on — and a position. Positions are fractions of the page rather than points or pixels, so the same values land correctly on A4, Letter or any other format. The origin is the page's top-left corner, and x and y mark the field's top-left corner, not its centre.

Value

Meaning

x

Distance from the left edge: 0 is the left edge, 1 the right

y

Distance from the top edge: 0 is the top edge, 1 the bottom

width

Field width, as a fraction of the page width

height

Field height, as a fraction of the page height

There are two practical ways to arrive at the numbers. The first is to copy them from a document prepared in the Circularo web app: place the fields there visually and send the document, then look it up with GET /documents, take its transaction.id and read every field's exact position from GET /transactions/{id} — the call this page makes below. This is the quickest route to a one-off layout, and the only sensible one for reproducing a layout somebody else designed.

The second is to compute them from a template you generate yourself, where the geometry is already known: divide each field's offset and size by the page size. A signature box roughly 0.25 wide and 0.08 high is comfortable to sign in. Keep fields away from the page edges and from each other, so that none of them overlaps a neighbour or covers text that has to stay readable.

Tip

Fields can also be positioned relative to a phrase in the document instead of fixed coordinates, so that a layout survives a change in the text — Position fields with anchor text.

Step 1: Place the fields

Send the document with one field of each kind placed for the signer:

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

{
    "document": {
        "title": "Employment Agreement",
        "fileContentBase64": "JVBERi0xLjQKJeLjz9MK..."
    },
    "send": {
        "recipients": [
            {
                "recipient": "jane.doe@example.com",
                "purpose": "sign"
            }
        ],
        "signatureFields": [
            {
                "recipients": [
                    "jane.doe@example.com"
                ],
                "type": "signature",
                "pages": [
                    1
                ],
                "tooltip": "Sign here",
                "position": {
                    "x": 0.6,
                    "y": 0.8,
                    "width": 0.25,
                    "height": 0.08
                }
            }
        ],
        "annotationFields": [
            {
                "recipients": [
                    "jane.doe@example.com"
                ],
                "type": "annotation_generic",
                "pages": [
                    1
                ],
                "required": true,
                "tooltip": "Enter your job title",
                "position": {
                    "x": 0.15,
                    "y": 0.8,
                    "width": 0.3,
                    "height": 0.04
                }
            },
            {
                "recipients": [
                    "jane.doe@example.com"
                ],
                "type": "annotation_generic",
                "pages": [
                    1
                ],
                "readOnly": true,
                "required": true,
                "text": "Employment Agreement — 2026",
                "position": {
                    "x": 0.15,
                    "y": 0.1,
                    "width": 0.5,
                    "height": 0.05
                }
            }
        ],
        "imageFields": [
            {
                "recipients": [
                    "jane.doe@example.com"
                ],
                "pages": [
                    1
                ],
                "tooltip": "Attach a photo of your ID badge",
                "position": {
                    "x": 0.6,
                    "y": 0.1,
                    "width": 0.2,
                    "height": 0.12
                }
            }
        ]
    }
}

The three field arrays collect different things:

  • signatureFields — where the recipient signs. type selects what is collected, a signature or initials.

  • annotationFields — fields the recipient fills in. annotation_generic collects free-form text; the API reference lists the other kinds, such as dates and checkboxes. A field with readOnly set to true and a text value is a fixed label the recipient cannot edit — use it to stamp a title or a note on the page.

  • imageFields — where the recipient attaches an image, such as a scan or a photo.

Each field also takes a tooltip, the hint the recipient sees while filling it, and required, which decides whether they can finish without it.

HTTP/2 201

{
    "signatureFields": [
        {
            "id": "Fz3TdR7yQn1wLp8H",
            "isFilled": false,
            "order": null,
            "pages": [
                1
            ],
            "position": {
                "height": 0.08,
                "width": 0.25,
                "x": 0.6,
                "y": 0.8
            },
            "recipients": [
                "jane.doe@example.com"
            ],
            "required": true,
            "tooltip": "Sign here",
            "type": "signature"
        }
    ],
    ...
}

Every placed field comes back with a generated id and its placement echoed; annotationFields and imageFields return the same way. Keep the ids — they identify the field in everything that follows.

Step 2: Follow the fields while the signer works

The transaction reports the placement at any time, including how far the recipients have got:

GET /transactions/Vt7RfKq2LmXe5ZwGyB4N
Authorization: Bearer YOUR_API_KEY
HTTP/2 200

{
    "status": "completed",
    "signatureFields": [
        {
            "id": "Fz3TdR7yQn1wLp8H",
            "isFilled": true,
            "order": null,
            "pages": [
                1
            ],
            "position": {
                "height": 0.08,
                "width": 0.25,
                "x": 0.6,
                "y": 0.8
            },
            "recipients": [
                "jane.doe@example.com"
            ],
            "required": true,
            "tooltip": "Sign here",
            "type": "signature"
        }
    ],
    ...
}

The signature field now reports isFilled as true, and so do the annotation and image fields (omitted here). With the last mandatory recipient done, the transaction status is completed.

Step 3: Read what the signer entered

The transaction tells you where the fields are and whether they are filled. What the signer actually put into them belongs to the document:

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

{
    "signatures": [
        {
            "comment": null,
            "signatureProvider": "internal",
            "signedAt": "2026-05-11T09:30:00.000Z",
            "signedBy": "jane.doe@example.com",
            "type": "signature"
        }
    ],
    "annotations": [
        {
            "addedAt": "2026-05-11T09:30:00.000Z",
            "addedBy": "jane.doe@example.com",
            "comment": null,
            "text": "Employment Agreement — 2026",
            "type": "annotation_generic"
        },
        {
            "addedAt": "2026-05-11T09:30:00.000Z",
            "addedBy": "jane.doe@example.com",
            "comment": null,
            "text": "Head of Engineering",
            "type": "annotation_generic"
        }
    ],
    "images": [
        {
            "addedAt": "2026-05-11T09:30:00.000Z",
            "addedBy": "jane.doe@example.com",
            "comment": null,
            "fileId": "da09a47b4f5b1xv726cfm36l8k1hkxzwmj5mszvfq6szoambdvc44v6i8hrhnu1f"
        }
    ],
    ...
}

Each entry names who added it and when. annotations carry the text — the job title the signer typed, and the fixed label stamped along with it — and images identify the file the signer attached. All of it is rendered into the signed document as well.

Complete example

Placing the fields and collecting what the signer entered, 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 pdfBase64 = (await fs.readFile("employment-agreement.pdf")).toString("base64");

const createRes = await fetch(`${BASE_URL}/transactions`, {
    method: "POST",
    headers: { ...AUTH_HEADER, "Content-Type": "application/json" },
    body: JSON.stringify({
        document: {
            title: "Employment Agreement",
            fileContentBase64: pdfBase64
        },
        send: {
            recipients: [
                {
                    recipient: "jane.doe@example.com",
                    purpose: "sign"
                }
            ],
            signatureFields: [
                {
                    recipients: [
                        "jane.doe@example.com"
                    ],
                    type: "signature",
                    pages: [
                        1
                    ],
                    tooltip: "Sign here",
                    position: {
                        x: 0.6,
                        y: 0.8,
                        width: 0.25,
                        height: 0.08
                    }
                }
            ],
            annotationFields: [
                {
                    recipients: [
                        "jane.doe@example.com"
                    ],
                    type: "annotation_generic",
                    pages: [
                        1
                    ],
                    required: true,
                    tooltip: "Enter your job title",
                    position: {
                        x: 0.15,
                        y: 0.8,
                        width: 0.3,
                        height: 0.04
                    }
                },
                {
                    recipients: [
                        "jane.doe@example.com"
                    ],
                    type: "annotation_generic",
                    pages: [
                        1
                    ],
                    readOnly: true,
                    required: true,
                    text: "Employment Agreement — 2026",
                    position: {
                        x: 0.15,
                        y: 0.1,
                        width: 0.5,
                        height: 0.05
                    }
                }
            ],
            imageFields: [
                {
                    recipients: [
                        "jane.doe@example.com"
                    ],
                    pages: [
                        1
                    ],
                    tooltip: "Attach a photo of your ID badge",
                    position: {
                        x: 0.6,
                        y: 0.1,
                        width: 0.2,
                        height: 0.12
                    }
                }
            ]
        }
    })
});
if (!createRes.ok) throw new Error(`Transaction failed: ${createRes.status}`); // error handling kept minimal for brevity

const transaction = await createRes.json();
for (const field of [...transaction.signatureFields, ...transaction.annotationFields, ...transaction.imageFields]) {
    console.log(`Placed ${field.id} for ${field.recipients.join(", ")} on page ${field.pages.join(", ")}`);
}

// wait for the signer to finish, then read the values they entered off the document
let state = transaction;
while (state.status === "in-progress") {
    await new Promise((resolve) => setTimeout(resolve, 30_000));
    state = await (await fetch(`${BASE_URL}/transactions/${transaction.id}`, { headers: AUTH_HEADER })).json();
}

const signedDocument = await (await fetch(`${BASE_URL}/documents/${transaction.document.id}`, { headers: AUTH_HEADER })).json();
for (const annotation of signedDocument.annotations) {
    console.log(`${annotation.addedBy} entered: ${annotation.text}`);
}

Next steps