The earlier pages each covered one part of sending a document for signature. This one puts them together: you sign the agreement yourself, a new hire with no Circularo account signs it through a link and fills in their job title, either of two managers approves, and HR countersigns. Six calls take it from a PDF to a signed file on your disk.
Before you begin
-
You have read the rest of this chapter. Everything here is explained in detail on the page it belongs to; this page shows how the pieces combine.
-
Your signature image is configured in the Circularo web app (Sign a document yourself), and applying it needs
use_signon your own account. -
Two things the recipients below depend on: sending to somebody without a Circularo account requires that your account may share outside the organization (
settings.canShareWithExternalUsersinGET /me), and the e-mail code protecting the new hire needs theshare_mail_otpright.
Step 1: Send the agreement and sign it in the same call
One request creates the document, applies your own signature to it, and routes it through everybody else:
POST /transactions
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{
"document": {
"title": "Employment Agreement",
"fileContentBase64": "JVBERi0xLjQKJeLjAM/T..."
},
"apply": {
"signatures": [
{
"fileId": "da09a47b4f5b1xv726cfm36l8k1hkxzwmj5mszvfq6szoambdvc44v6i8hrhnu1f",
"pages": [
1
],
"position": {
"x": 0.6,
"y": 0.66,
"width": 0.25,
"height": 0.06
}
}
]
},
"send": {
"sequential": true,
"message": "Please review and sign the renewed employment agreement.",
"recipients": [
{
"recipient": "jane.doe@example.com",
"purpose": "sign",
"order": 1,
"verification": {
"email": {}
}
},
{
"recipient": "robert.brown@example.com",
"purpose": "approve",
"order": 2,
"quorum": 1
},
{
"recipient": "emma.wilson@example.com",
"purpose": "approve",
"order": 2,
"quorum": 1
},
{
"recipient": "alex.novak@example.com",
"purpose": "sign",
"order": 3
}
],
"signatureFields": [
{
"recipients": [
"jane.doe@example.com"
],
"pages": [
1
],
"position": {
"x": 0.12,
"y": 0.66,
"width": 0.25,
"height": 0.06
}
},
{
"recipients": [
"alex.novak@example.com"
],
"pages": [
1
],
"position": {
"x": 0.6,
"y": 0.78,
"width": 0.25,
"height": 0.06
}
}
],
"annotationFields": [
{
"recipients": [
"jane.doe@example.com"
],
"pages": [
1
],
"tooltip": "Enter your job title",
"position": {
"x": 0.12,
"y": 0.78,
"width": 0.3,
"height": 0.04
}
}
]
}
}
Four things are happening at once here:
-
applyputs your signature on the document straight away, before anybody else sees it. -
send.recipientsact in rounds becausesequentialistrue. The new hire signs in round1, the two managers share round2with aquorumof1— whichever of them approves first settles the round for both — and HR countersigns in round3. -
The new hire has no Circularo account, so their access is protected by a one-time code sent to their address. That protection is what lets you hand them a link in the next step.
-
signatureFieldsgives the new hire and HR a signature box each, on their own line of the agreement, andannotationFieldsadds a form field where the new hire types their job title. Everyone who has to sign knows exactly where.
HTTP/2 201
{
"id": "Vt7RfKq2LmXe5ZwGyB4N",
"status": "in-progress",
"document": {
"id": "Dq4RfKp2LmXe5ZwGyB4N",
"mainFileId": "b71f0c9de2a34c7f8de51a2c6b90f4e37c18a5d0429e6bb3f1c7a8d25e93b60a4",
"metadataDefinition": "d_default",
"signatureProvider": "internal",
"templateId": null,
"title": "Employment Agreement"
},
"recipients": [
{
"id": "Gk5cPdM9XvT3qHnU7jWs",
"recipient": "jane.doe@example.com",
"purpose": "sign",
"order": 1,
"quorum": null,
"status": "pending",
...
},
{
"id": "Wm5kBq2vNp9xLc4HtYeD",
"recipient": "emma.wilson@example.com",
"purpose": "approve",
"order": 2,
"quorum": 1,
"status": "queued",
...
},
{
"id": "Rj8dGt3yQs6nZb1HxKmA",
"recipient": "robert.brown@example.com",
"purpose": "approve",
"order": 2,
"quorum": 1,
"status": "queued",
...
},
{
"id": "Lp2WdN8qYt5vHmC4bRfZ",
"recipient": "alex.novak@example.com",
"purpose": "sign",
"order": 3,
"quorum": null,
"status": "queued",
...
},
{
"id": null,
"recipient": "john.smith@example.com",
"purpose": "sign",
"order": null,
"quorum": null,
"status": "completed",
...
}
],
...
}
You appear among the recipients as an already completed signer — your signature is on the document, but the transaction runs on for everybody else. The new hire is pending; the rest are queued behind them.
Step 2: Hand the new hire their link
POST /transactions/Vt7RfKq2LmXe5ZwGyB4N/recipients/Gk5cPdM9XvT3qHnU7jWs/link
Authorization: Bearer YOUR_API_KEY
HTTP/2 201
{
"shareToken": "82c5fa30d26bc80b828c2c6e8ced7f2655b0f1539a25a8ac888ccc75b30363f0",
"shareUrl": "https://sandbox.circularo.com/share?shareToken=82c5fa30d26bc80b828c2c6e8ced7f2655b0f1539a25a8ac888ccc75b30363f0"
}
Deliver shareUrl however suits your onboarding — the code they receive by e-mail is what proves it is them.
Step 3: Watch the rounds advance
The new hire signs and fills their field. Reading the transaction shows what that did:
GET /transactions/Vt7RfKq2LmXe5ZwGyB4N
Authorization: Bearer YOUR_API_KEY
HTTP/2 200
{
"id": "Vt7RfKq2LmXe5ZwGyB4N",
"status": "in-progress",
"recipients": [
{
"recipient": "jane.doe@example.com",
"order": 1,
"status": "completed",
...
},
{
"recipient": "emma.wilson@example.com",
"order": 2,
"status": "pending",
...
},
{
"recipient": "robert.brown@example.com",
"order": 2,
"status": "pending",
...
},
{
"recipient": "alex.novak@example.com",
"order": 3,
"status": "queued",
...
},
{
"recipient": "john.smith@example.com",
"order": null,
"status": "completed",
...
}
],
...
}
They are completed, and both managers turned pending at once: a round opens for everybody in it, even when only one of them has to act. HR stays queued for their own round.
Step 4: See the finished transaction
One manager approves — enough for the quorum — and HR countersigns:
GET /transactions/Vt7RfKq2LmXe5ZwGyB4N
Authorization: Bearer YOUR_API_KEY
HTTP/2 200
{
"id": "Vt7RfKq2LmXe5ZwGyB4N",
"status": "completed",
"completedAt": "2026-05-13T15:45:00.000Z",
"document": {
"id": "Dq4RfKp2LmXe5ZwGyB4N",
"mainFileId": "c93e7a1d05b64f28ae3c7d9b06f15a824e0d3b7c9a15f6e820b4d7c3a95e61f0",
"metadataDefinition": "d_default",
"signatureProvider": "internal",
"templateId": null,
"title": "Employment Agreement"
},
"recipients": [
{
"recipient": "jane.doe@example.com",
"purpose": "sign",
"status": "completed",
"completedAt": "2026-05-12T14:05:00.000Z",
...
},
{
"recipient": "emma.wilson@example.com",
"purpose": "approve",
"status": "superseded",
"completedAt": null,
...
},
{
"recipient": "robert.brown@example.com",
"purpose": "approve",
"status": "completed",
"completedAt": "2026-05-13T08:20:00.000Z",
...
},
{
"recipient": "alex.novak@example.com",
"purpose": "sign",
"status": "completed",
"completedAt": "2026-05-13T15:45:00.000Z",
...
},
{
"recipient": "john.smith@example.com",
"purpose": "sign",
"status": "completed",
"completedAt": "2026-05-11T09:30:00.000Z",
...
}
],
...
}
The transaction is completed. The manager who acted is completed; the other is superseded — the quorum was met without them, so they were never required to act and have no completedAt. Note document.mainFileId: it is not the file you uploaded, because every signature produces a new one.
Step 5: Read what ended up on the document
GET /documents/Dq4RfKp2LmXe5ZwGyB4N
Authorization: Bearer YOUR_API_KEY
HTTP/2 200
{
"id": "Dq4RfKp2LmXe5ZwGyB4N",
"title": "Employment Agreement",
"signatures": [
{
"comment": null,
"signatureProvider": "internal",
"signedAt": "2026-05-11T09:30:00.000Z",
"signedBy": "john.smith@example.com",
"type": "signature"
},
{
"comment": null,
"signatureProvider": "internal",
"signedAt": "2026-05-12T14:05:00.000Z",
"signedBy": "jane.doe@example.com",
"type": "signature"
},
{
"comment": null,
"signatureProvider": "internal",
"signedAt": "2026-05-13T15:45:00.000Z",
"signedBy": "alex.novak@example.com",
"type": "signature"
}
],
"annotations": [
{
"addedAt": "2026-05-12T14:05:00.000Z",
"addedBy": "jane.doe@example.com",
"comment": null,
"text": "Head of Engineering",
"type": "annotation_generic"
}
],
...
}
Three signatures, one for each person who signed — the approval is not among them, because approving records a decision rather than a signature. The job title the new hire typed is stored as an annotation. This is where the values recipients entered live: the transaction tracks the process, the document holds the result.
Step 6: Download the signed agreement
GET /files/c93e7a1d05b64f28ae3c7d9b06f15a824e0d3b7c9a15f6e820b4d7c3a95e61f0/content
Authorization: Bearer YOUR_API_KEY
HTTP/2 200
Content-Type: application/pdf
The response body is the signed PDF itself. Store it wherever your process keeps agreements — it is the artifact the whole flow existed to produce.
Complete example
The whole agreement as a Node.js script: send it signed, hand the new hire their link, wait for the rounds, keep the result. Error handling is minimal for brevity — in production, inspect the error envelope of non-2xx responses.
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 me = await (await fetch(`${BASE_URL}/me`, { headers: AUTH_HEADER })).json();
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
},
apply: {
signatures: [
{
fileId: me.signatures[0].fileId,
pages: [
1
],
position: {
x: 0.6,
y: 0.66,
width: 0.25,
height: 0.06
}
}
]
},
send: {
sequential: true,
message: "Please review and sign the renewed employment agreement.",
recipients: [
{
recipient: "jane.doe@example.com",
purpose: "sign",
order: 1,
verification: {
email: {}
}
},
{
recipient: "robert.brown@example.com",
purpose: "approve",
order: 2,
quorum: 1
},
{
recipient: "emma.wilson@example.com",
purpose: "approve",
order: 2,
quorum: 1
},
{
recipient: "alex.novak@example.com",
purpose: "sign",
order: 3
}
],
signatureFields: [
{
recipients: [
"jane.doe@example.com"
],
pages: [
1
],
position: {
x: 0.12,
y: 0.66,
width: 0.25,
height: 0.06
}
},
{
recipients: [
"alex.novak@example.com"
],
pages: [
1
],
position: {
x: 0.6,
y: 0.78,
width: 0.25,
height: 0.06
}
}
],
annotationFields: [
{
recipients: [
"jane.doe@example.com"
],
pages: [
1
],
tooltip: "Enter your job title",
position: {
x: 0.12,
y: 0.78,
width: 0.3,
height: 0.04
}
}
]
}
})
});
if (!createRes.ok) throw new Error(`Transaction failed: ${createRes.status}`); // error handling kept minimal for brevity
let transaction = await createRes.json();
// the new hire has no account, so onboarding delivers their link instead of e-mail
const newHire = transaction.recipients.find((recipient) => recipient.order === 1);
const linkRes = await fetch(`${BASE_URL}/transactions/${transaction.id}/recipients/${newHire.id}/link`, {
method: "POST",
headers: AUTH_HEADER
});
console.log(`Send ${newHire.recipient} to ${(await linkRes.json()).shareUrl}`);
while (transaction.status === "in-progress") {
await new Promise((resolve) => setTimeout(resolve, 60_000));
transaction = await (await fetch(`${BASE_URL}/transactions/${transaction.id}`, { headers: AUTH_HEADER })).json();
}
// anything other than completed means somebody refused or a deadline passed
if (transaction.status !== "completed") throw new Error(`The agreement ended as ${transaction.status}`);
const signed = await fetch(`${BASE_URL}/files/${transaction.document.mainFileId}/content`, { headers: AUTH_HEADER });
await fs.writeFile("employment-agreement-signed.pdf", Buffer.from(await signed.arrayBuffer()));
Next steps
-
Collect the evidence behind this result: the audit trail and the certificate of fulfillment — Collect the evidence of a signed document.
-
Stop polling for the changes this page watched — React to events with webhooks.