React to events with webhooks

Polling tells you what changed only as often as you ask. A webhook has Circularo call you instead, the moment a document event happens in your organization. Registering one takes three calls; receiving deliveries safely is the rest of this guide.

Before you begin

  • An organization-administrator key. A webhook belongs to the whole organization, so a key issued to a regular member is refused with 403.

  • An HTTPS endpoint that accepts POST requests. Circularo delivers to https URLs only; an http callback is rejected with 400.

What a delivery looks like

Circularo sends a JSON POST to your callback URL with four fields, and nothing else:

Field

What it holds

documentId

The document the event happened to

event

What happened, as a name like documentCreated, signRequest, documentSigned or documentCompleted

actor

Who caused it

timestamp

When it happened

The notification carries no document content — it is a pointer. Read the document itself when you need more than the fact that something changed.

Branch on the events you care about and ignore the rest: further event names exist and more can appear, so a receiver that treats an unrecognized event as a no-op keeps working.

Step 1: Register the webhook

Give Circularo the URL, and a secret if you want the deliveries signed:

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

{
    "callbackUrl": "https://example.com/webhooks/circularo",
    "hmacSecret": "s3cr3t-signing-key"
}
HTTP/2 201

{
    "callbackUrl": "https://example.com/webhooks/circularo",
    "createdAt": "2026-05-11T09:30:00.000Z",
    "createdBy": "john.smith@example.com",
    "id": "Zq4tXm7RkP2nWv5bLdCy",
    "isHmacConfigured": true
}

isHmacConfigured confirms the secret was stored. The secret itself is write-only — it is never returned again, here or anywhere else. There is no way to change it later either: to rotate a secret, register a new webhook and delete the old one.

Keep the id; it is what you delete the webhook by.

Step 2: See the webhooks you have

The organization's webhooks come back as an ordinary paged collection:

GET /webhooks
Authorization: Bearer YOUR_API_KEY
HTTP/2 200

{
    "pagination": {
        "limit": 20,
        "nextCursor": null,
        "offset": 0
    },
    "results": [
        {
            "callbackUrl": "https://example.com/webhooks/circularo",
            "createdAt": "2026-05-11T09:30:00.000Z",
            "createdBy": "john.smith@example.com",
            "id": "Zq4tXm7RkP2nWv5bLdCy",
            "isHmacConfigured": true
        }
    ],
    "total": 1
}

Use this to reconcile what your integration believes it registered with what Circularo will actually call — and to find the id of a webhook somebody registered before you.

Step 3: Remove the webhook

Deleting a webhook stops the deliveries:

DELETE /webhooks/Zq4tXm7RkP2nWv5bLdCy
Authorization: Bearer YOUR_API_KEY
HTTP/2 204

Complete example

Two things decide whether a webhook integration is trustworthy, and the receiver below does both.

It verifies the signature, because a callback URL is reachable by anyone and a request arriving there proves nothing on its own. When a secret is configured, every delivery carries an X-Circularo-Signature header holding the HMAC-SHA256 of the raw request body, hex-encoded. Compute the same HMAC over the bytes you received — before parsing them, since re-serializing JSON changes them — and compare the two with a timing-safe comparison.

And it answers immediately, before doing any work of its own. A delivery is attempted once and is not repeated: a receiver that fails, or is too slow to answer, loses that notification.

Important

Without a secret the deliveries are unsigned, and your endpoint cannot tell a genuine notification from a forged one. Configure a secret whenever the webhook triggers anything that matters.

JavaScript
import crypto from "node:crypto";
import http from "node:http";

const SECRET = "s3cr3t-signing-key"; // the hmacSecret the webhook was registered with

const verify = (raw, signature) => {
    const expected = crypto.createHmac("sha256", SECRET).update(raw).digest("hex");
    const received = Buffer.from(signature ?? "", "hex");
    // timing-safe comparison, and equal length is required for it
    return (received.length === expected.length / 2) && crypto.timingSafeEqual(Buffer.from(expected, "hex"), received);
};

http.createServer((req, res) => {
    const chunks = [];
    req.on("data", (chunk) => chunks.push(chunk));
    req.on("end", () => {
        const raw = Buffer.concat(chunks);
        if (!verify(raw, req.headers["x-circularo-signature"])) {
            res.writeHead(401).end();
            return;
        }

        const event = JSON.parse(raw.toString());
        // acknowledge first, process afterwards - the delivery is never repeated
        res.writeHead(204).end();

        if (event.event === "documentCompleted") {
            queue.push(event.documentId); // your own queue, worker, outbox...
        }
    });
}).listen(8080);

Next steps

  • Watch a transaction move through its rounds, which is what most events report — Manage a running transaction.

  • Read the document a notification points at — Search documents.

  • The webhook endpoints and their statuses are documented in the API reference.