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

GET/api/v1/webhooks
Auth required
Returns your webhooks with the secret field redacted (the raw secret is only returned once, on create). Any authenticated user.

#Register a webhook

POST/api/v1/webhooks
Auth requiredPro
Creates the webhook and surfaces the raw signing secret in the response body — exactly once. Tier limits: Pro allows 1 webhook; Capper tiers are uncapped.
ts
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

GET/api/v1/webhooks/{id}
Auth required
Detail view. Owner-scoped — non-owners get 404 not_found (never 403) to avoid leaking ownership.
PATCH/api/v1/webhooks/{id}
Auth required
Update URL, events, filters, or white-label settings.
DELETE/api/v1/webhooks/{id}
Auth required
Removes the webhook permanently.

#Deliveries + test fire

GET/api/v1/webhooks/{id}/deliveries
Auth required
Paginated delivery history — status, timestamp, response code, body.
POST/api/v1/webhooks/{id}/test
Auth required
Fires 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:

http
Content-Type: application/json
Closeline-Signature: t=1729195200,v1=ab12...
Closeline-Event: signal.fired
Closeline-Delivery-Id: evt_01HXY...

Body:

json
{
  "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

ts
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.