Position fields with anchor text

Coordinates tie a field to a spot on the page. Anchors tie it to a phrase in the text instead, so the same integration keeps working when the contract grows a paragraph and the signature line moves down. This guide uploads an agreement with two anchors and sends it with a signature field on each of the positions Circularo calculated. It takes two calls.

Before you begin

  • The document is a PDF with a text layer. Anchors are found in the document's text. A scan with no text layer has nothing to match.

How anchors work

You give each anchor an id of your choosing, the text to look for, and an offset that says where the field goes relative to that text. Circularo searches the document and returns the calculated position for every place the text was found.

The search runs line by line and ignores letter case. Use text for an exact phrase or regexp for a pattern. The offset is measured from the top-left corner of the first character of the match, and — like every position in the API — its values are fractions of the page:

Value

Meaning

x

Horizontal shift from the match: positive moves right, negative left

y

Vertical shift from the match: positive moves down, negative up

width

Width of the field to be placed

height

Height of the field to be placed

An offset of 0 on both axes puts the field directly on top of the anchor text, which is rarely what you want — shift it above, below or beside the phrase.

Step 1: Upload the document with anchors

The agreement ends with two lines, Employee signature: and Employer signature:. Anchor a field above each of them:

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

fileName: Employment Agreement
signatureAnchors[0][id]: employee-signature
signatureAnchors[0][text]: Employee signature:
signatureAnchors[0][offset][x]: 0
signatureAnchors[0][offset][y]: -0.07
signatureAnchors[0][offset][width]: 0.25
signatureAnchors[0][offset][height]: 0.06
signatureAnchors[1][id]: employer-signature
signatureAnchors[1][text]: Employer signature:
signatureAnchors[1][offset][x]: 0
signatureAnchors[1][offset][y]: -0.07
signatureAnchors[1][offset][width]: 0.25
signatureAnchors[1][offset][height]: 0.06
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 "signatureAnchors[0][id]=employee-signature" \
    -F "signatureAnchors[0][text]=Employee signature:" \
    -F "signatureAnchors[0][offset][x]=0" \
    -F "signatureAnchors[0][offset][y]=-0.07" \
    -F "signatureAnchors[0][offset][width]=0.25" \
    -F "signatureAnchors[0][offset][height]=0.06" \
    -F "signatureAnchors[1][id]=employer-signature" \
    -F "signatureAnchors[1][text]=Employer signature:" \
    -F "signatureAnchors[1][offset][x]=0" \
    -F "signatureAnchors[1][offset][y]=-0.07" \
    -F "signatureAnchors[1][offset][width]=0.25" \
    -F "signatureAnchors[1][offset][height]=0.06" \
    -F "file=@employment-agreement.pdf"
HTTP/2 201

{
    "id": "9f2c41b6a8d7e05c3b19f7a24e8d60c1",
    "fileName": "Employment Agreement",
    "pageCount": 1,
    "anchorPositions": [
        {
            "id": "employee-signature",
            "matches": [
                {
                    "page": 1,
                    "position": {
                        "height": 0.06,
                        "width": 0.25,
                        "x": 0.11764706,
                        "y": 0.66116625
                    }
                }
            ]
        },
        {
            "id": "employer-signature",
            "matches": [
                {
                    "page": 1,
                    "position": {
                        "height": 0.06,
                        "width": 0.25,
                        "x": 0.5546219,
                        "y": 0.66116625
                    }
                }
            ]
        }
    ],
    ...
}

Every anchor you sent is answered, in the order you sent it, under the id you gave it — with one entry in matches per place the text was found. Here that is one each, both on page 1. The two positions share the same y and differ in x, exactly like the two signature lines on the page.

Important

An anchor whose text is nowhere in the document is not an error: it comes back with an empty matches list. Check for that before you build the transaction — it means the document does not look the way you expected, and no field would be placed.

Step 2: Place a field on each calculated position

A matches entry's position has exactly the shape a placed field expects, so it goes into signatureFields as it is:

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

{
    "document": {
        "title": "Employment Agreement",
        "fileId": "9f2c41b6a8d7e05c3b19f7a24e8d60c1"
    },
    "send": {
        "recipients": [
            {
                "recipient": "jane.doe@example.com",
                "purpose": "sign"
            },
            {
                "recipient": "robert.brown@example.com",
                "purpose": "sign"
            }
        ],
        "signatureFields": [
            {
                "recipients": [
                    "jane.doe@example.com"
                ],
                "pages": [
                    1
                ],
                "position": {
                    "height": 0.06,
                    "width": 0.25,
                    "x": 0.11764706,
                    "y": 0.66116625
                }
            },
            {
                "recipients": [
                    "robert.brown@example.com"
                ],
                "pages": [
                    1
                ],
                "position": {
                    "height": 0.06,
                    "width": 0.25,
                    "x": 0.5546219,
                    "y": 0.66116625
                }
            }
        ]
    }
}
HTTP/2 201

{
    "id": "Vt7RfKq2LmXe5ZwGyB4N",
    "status": "in-progress",
    "signatureFields": [
        {
            "id": "Fz3TdR7yQn1wLp8H",
            "isFilled": false,
            "order": null,
            "pages": [
                1
            ],
            "position": {
                "height": 0.06,
                "width": 0.25,
                "x": 0.11764706,
                "y": 0.66116625
            },
            "recipients": [
                "jane.doe@example.com"
            ],
            "required": true,
            "tooltip": null,
            "type": "signature"
        },
        {
            "id": "Nq6VbS2xJm4tKd7P",
            "isFilled": false,
            "order": null,
            "pages": [
                1
            ],
            "position": {
                "height": 0.06,
                "width": 0.25,
                "x": 0.5546219,
                "y": 0.66116625
            },
            "recipients": [
                "robert.brown@example.com"
            ],
            "required": true,
            "tooltip": null,
            "type": "signature"
        }
    ],
    ...
}

Both fields land on the anchor-derived positions, and the transaction starts. From here the flow is the one you already know: recipients act, and the transaction advances.

Tip

A regexp anchor is the way to place fields on a repeating structure — signature: matches both lines of this agreement and returns both positions, each with its page. Prefer the most specific phrase you can: a short, common word matches far more often than you intend.

Complete example

Uploading with anchors and sending the agreement, 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 form = new FormData();
form.set("fileName", "Employment Agreement");
form.set("file", new Blob([await fs.readFile("employment-agreement.pdf")]), "employment-agreement.pdf");
for (const [index, anchor] of [["employee-signature", "Employee signature:"], ["employer-signature", "Employer signature:"]].entries()) {
    form.set(`signatureAnchors[${index}][id]`, anchor[0]);
    form.set(`signatureAnchors[${index}][text]`, anchor[1]);
    form.set(`signatureAnchors[${index}][offset][x]`, "0");
    form.set(`signatureAnchors[${index}][offset][y]`, "-0.07");
    form.set(`signatureAnchors[${index}][offset][width]`, "0.25");
    form.set(`signatureAnchors[${index}][offset][height]`, "0.06");
}

const upload = await (await fetch(`${BASE_URL}/files`, { method: "POST", headers: AUTH_HEADER, body: form })).json();
const fileId = upload.id;

const positionOf = (anchorId) => {
    const anchor = upload.anchorPositions.find((item) => item.id === anchorId);
    if (anchor.matches.length === 0) throw new Error(`Anchor ${anchorId} was not found in the document`); // error handling kept minimal for brevity
    return anchor.matches[0].position;
};
const employeePosition = positionOf("employee-signature");
const employerPosition = positionOf("employer-signature");

const createRes = await fetch(`${BASE_URL}/transactions`, {
    method: "POST",
    headers: { ...AUTH_HEADER, "Content-Type": "application/json" },
    body: JSON.stringify({
        document: {
            title: "Employment Agreement",
            fileId: fileId
        },
        send: {
            recipients: [
                {
                    recipient: "jane.doe@example.com",
                    purpose: "sign"
                },
                {
                    recipient: "robert.brown@example.com",
                    purpose: "sign"
                }
            ],
            signatureFields: [
                {
                    recipients: [
                        "jane.doe@example.com"
                    ],
                    pages: [
                        1
                    ],
                    position: employeePosition
                },
                {
                    recipients: [
                        "robert.brown@example.com"
                    ],
                    pages: [
                        1
                    ],
                    position: employerPosition
                }
            ]
        }
    })
});
console.log(`Transaction ${(await createRes.json()).id} sent`);

Next steps