Webhooks
Subscribe your server to Closeline events. Every delivery is HMAC-signed with a per-webhook secret — verify every request before you act on it.
#List your webhooks
/api/v1/webhooksReturns your webhooks with the
secret field redacted (the raw secret is only returned once, on create). Any authenticated user.#Register a webhook
/api/v1/webhooksCreates the webhook and surfaces the raw signing secret in the response body — exactly once. Tier limits: Pro allows 1 webhook; Capper tiers are uncapped.
const created = await cl.webhooks.create({
url: "https://example.com/hooks/closeline",
events: ["signal.fired", "brief.published"],
});
// The raw secret is returned ONCE here — store it out-of-band.
console.log(created.secret);#Manage a webhook
/api/v1/webhooks/{id}Detail view. Owner-scoped — non-owners get
404 not_found (never 403) to avoid leaking ownership./api/v1/webhooks/{id}Update URL, events, filters, or white-label settings.
/api/v1/webhooks/{id}Removes the webhook permanently.
#Deliveries + test fire
/api/v1/webhooks/{id}/deliveriesPaginated delivery history — status, timestamp, response code, body.
/api/v1/webhooks/{id}/testFires a synthetic event so you can verify end-to-end signing before going live. Returns
{ delivery_id }.#Delivery shape
Closeline POSTs JSON to your URL with these headers:
Content-Type: application/json
Closeline-Signature: t=1729195200,v1=ab12...
Closeline-Event: signal.fired
Closeline-Delivery-Id: evt_01HXY...Body:
{
"id": "evt_01HXY...",
"type": "signal.fired",
"created_at": "2026-04-17T22:10:03Z",
"data": { }
}#HMAC signing
- Header:
Closeline-Signature: t=<unix_seconds>,v1=<hex>. - Compute:
hex( HMAC-SHA256(secret, t + "." + rawBody) ). - Reject timestamps outside a ±300s window (replay defense).
- Use a constant-time compare when verifying the hash.
- Verify against the raw body bytes — not a re-serialisation.
#Node verification example
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifySignature(
secret: string,
header: string, // value of Closeline-Signature
rawBody: string, // exact bytes received
): boolean {
// header looks like: "t=1729195200,v1=ab12..."
const parts = Object.fromEntries(
header.split(",").map((p) => p.split("=")),
);
const t = Number(parts.t);
const v1 = String(parts.v1);
if (!Number.isFinite(t) || !v1) return false;
// Reject replays older than 5 minutes.
if (Math.abs(Math.floor(Date.now() / 1000) - t) > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
if (expected.length !== v1.length) return false;
return timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}#Streaming alternative
If you don't want to host a webhook endpoint, subscribe to the same event feed over SSE — see Streaming.