Manage a running transaction

A transaction can be adjusted for as long as it is running. This guide follows one from checking on its progress through reminding its recipients, extending a deadline, replacing a recipient and finally cancelling it — five calls, each of which stands on its own.

Before you begin

  • You have a running transaction. Its identifier, and the identifier of each recipient taking part in it, come back from the call that created it — and from GET /transactions/{id} at any time afterwards.

Step 1: Check where the transaction stands

One call answers who has acted and who is holding things up:

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

{
    "id": "Vt7RfKq2LmXe5ZwGyB4N",
    "status": "in-progress",
    "sequential": false,
    "recipients": [
        {
            "completedAt": null,
            "expiresAt": null,
            "id": "Lp2WdN8qYt5vHmC4bRfZ",
            "isMandatory": true,
            "language": null,
            "message": null,
            "order": null,
            "permissions": [
                "print",
                "viewMetadata",
                "annotateOnReject",
                "rejectWithoutComment",
                "uploadFiles"
            ],
            "purpose": "approve",
            "quorum": null,
            "recipient": "robert.brown@example.com",
            "reminder": null,
            "status": "pending",
            "turnStartedAt": "2026-05-11T09:30:00.000Z",
            "verification": null
        },
        {
            "completedAt": null,
            "expiresAt": null,
            "id": "Gk5cPdM9XvT3qHnU7jWs",
            "isMandatory": true,
            "language": null,
            "message": null,
            "order": null,
            "permissions": [
                "print",
                "viewMetadata",
                "annotateOnReject",
                "rejectWithoutComment",
                "uploadFiles"
            ],
            "purpose": "sign",
            "quorum": null,
            "recipient": "jane.doe@example.com",
            "reminder": null,
            "status": "pending",
            "turnStartedAt": "2026-05-11T09:30:00.000Z",
            "verification": null
        }
    ],
    ...
}

The transaction is in-progress and both recipients are pending — this one was sent in parallel, so nobody is waiting for a turn. A recipient's completedAt fills in when they act, and status settles on what they did.

Step 2: Nudge everyone who has not acted

Reminding costs one call and reaches every recipient whose turn it is:

POST /transactions/Vt7RfKq2LmXe5ZwGyB4N/remind
Authorization: Bearer YOUR_API_KEY
HTTP/2 204

Nothing comes back — the reminders were sent. Recipients who already acted, and those still waiting for an earlier round, are left alone. To reach a single person instead, use POST /transactions/{id}/recipients/{recipientId}/remind.

Tip

A reminder you send by hand is a one-off. To have Circularo chase a recipient on its own, give them a reminder — as the next step does.

Step 3: Give a recipient more time

Recipient settings stay editable while their part is still open. Extend the deadline and let Circularo follow up every few days:

PATCH /transactions/Vt7RfKq2LmXe5ZwGyB4N/recipients/Gk5cPdM9XvT3qHnU7jWs
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

{
    "expiresAt": "2026-05-29T16:00:00.000Z",
    "reminder": {
        "startAt": "2026-05-18T08:00:00.000Z",
        "intervalDays": 3
    }
}
HTTP/2 200

{
    "id": "Gk5cPdM9XvT3qHnU7jWs",
    "recipient": "jane.doe@example.com",
    "purpose": "sign",
    "status": "pending",
    "expiresAt": "2026-05-29T16:00:00.000Z",
    "reminder": {
        "intervalDays": 3,
        "startAt": "2026-05-18T08:00:00.000Z"
    },
    ...
}

The participation keeps its id and its place in the transaction; only what you sent has changed. The same call adjusts permissions and verification, and follows the usual rule for a PATCH: leave a field out to keep it, send null to restore its default.

Step 4: Replace a recipient who cannot act

When the right person is somebody else, hand the part over rather than starting again:

POST /transactions/Vt7RfKq2LmXe5ZwGyB4N/recipients/Lp2WdN8qYt5vHmC4bRfZ/reassign
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

{
    "recipient": "emma.wilson@example.com"
}
HTTP/2 200

{
    "id": "Rj8dGt3yQs6nZb1HxKmA",
    "recipient": "emma.wilson@example.com",
    "purpose": "approve",
    "status": "pending",
    ...
}

The new recipient inherits the purpose, the routing and the fields placed for their predecessor, and any setting you send alongside recipient overrides what was inherited. Note the id: the replacement takes part under a new participation, and the one it replaced disappears from the transaction. Store the returned id in place of the old one.

When nobody should take the part over, drop the participation instead: DELETE /transactions/{id}/recipients/{recipientId} withdraws that recipient's pending action and their access to the document, while the transaction carries on with everyone else. Send a message with it to explain the removal in the notification they receive. A removal that would leave the transaction with nothing to wait for is refused with 400 — a mandatory recipient, the last recipient whose action is awaited, or one whose departure drops their round below its quorum. That case is a cancellation, which the next step covers.

Step 5: Call the whole thing off

Cancelling ends the transaction for everybody at once:

POST /transactions/Vt7RfKq2LmXe5ZwGyB4N/cancel
Authorization: Bearer YOUR_API_KEY
HTTP/2 200

{
    "id": "Vt7RfKq2LmXe5ZwGyB4N",
    "status": "cancelled",
    ...
}

The transaction status is cancelled — a terminal state, so reminding, editing or cancelling again is refused with 409. Recipients who never acted keep the status they had at that moment; the transaction's own status is the authoritative answer to "is anything still going to happen here".

Two endings arrive without you asking for them, and both close the transaction the same way. A recipient who refuses ends it as rejected for everybody, with their own status rejected and their reason in the document's history; a deadline that passes ends it as expired. Treat every terminal status as the end of your work on that transaction, rather than waiting for the completed you were hoping for.

Complete example

Watching a transaction and stepping in, as a Node.js script. Error handling is minimal for brevity — in production, inspect the error envelope of non-2xx responses.

JavaScript
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 JSON_HEADERS = { ...AUTH_HEADER, "Content-Type": "application/json" };

const transactionId = "Vt7RfKq2LmXe5ZwGyB4N"; // kept from the call that created it
const deadline = new Date(Date.now() + 14 * 24 * 3600_000).toISOString();

const transaction = await (await fetch(`${BASE_URL}/transactions/${transactionId}`, { headers: AUTH_HEADER })).json();
const waiting = transaction.recipients.filter((recipient) => recipient.status === "pending");
console.log(`${transaction.status}, waiting for ${waiting.map((recipient) => recipient.recipient).join(", ") || "nobody"}`);

if (waiting.length > 0) {
    await fetch(`${BASE_URL}/transactions/${transactionId}/remind`, { method: "POST", headers: AUTH_HEADER });

    // give the people who are holding things up until the new deadline
    for (const recipient of waiting) {
        await fetch(`${BASE_URL}/transactions/${transactionId}/recipients/${recipient.id}`, {
            method: "PATCH",
            headers: JSON_HEADERS,
            body: JSON.stringify({ expiresAt: deadline })
        });
    }
}

Next steps