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
httpsURLs only; anhttpcallback is rejected with400.
What a delivery looks like
Circularo sends a JSON POST to your callback URL with four fields, and nothing else:
|
Field |
What it holds |
|---|---|
|
|
The document the event happened to |
|
|
What happened, as a name like |
|
|
Who caused it |
|
|
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.
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.
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.