Clarion
Sign in Get started
Docs · API & developers

Verify Clarion webhook signatures (X-Clarion-Signature-V2)

Verify the X-Clarion-Signature-V2 HMAC-SHA256 header on Clarion webhooks, reject stale timestamps, and deduplicate retries on X-Clarion-Delivery.

Anyone who learns your webhook URL can send it a request. Checking the signature proves a request came from Clarion and hasn’t been changed. Checking its timestamp stops someone replaying a request they captured. Do both before acting on any webhook.

You need the subscription’s signing secret (whsec_…), shown once when you set up the webhook.

The header

X-Clarion-Signature-V2: t=1759823052,v1=9c4e0f...e71a
  • t is the Unix time, in seconds, when this attempt was sent. It’s also in X-Clarion-Timestamp.
  • v1 is the hex HMAC-SHA256, keyed with your signing secret, of the string `${t}.${rawBody}` — the timestamp, a full stop, and the request body exactly as received.

Verify it

  1. Read the raw request body before any JSON parsing. Re-serialising parsed JSON changes whitespace and breaks the comparison.
  2. Split the header on commas and read t and v1.
  3. Reject the request if t is more than 300 seconds from your current time.
  4. Compute the HMAC of `${t}.${rawBody}` with your secret.
  5. Compare it to v1 in constant time.
  6. Check X-Clarion-Delivery against the ids you’ve already handled, and skip duplicates.

Node.js

const crypto = require("crypto");

function verify(rawBody, headers, secret, toleranceSeconds = 300) {
  const header = headers["x-clarion-signature-v2"] || "";
  const parts = Object.fromEntries(
    header.split(",").map((p) => p.split("=").map((s) => s.trim())),
  );
  const t = Number(parts.t);
  if (!Number.isFinite(t) || !parts.v1) return false;

  // Reject anything outside the window, so a captured request stops working.
  if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");

  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(parts.v1, "hex");
  // Constant-time — a plain === leaks the signature a byte at a time.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

With Express, capture the raw body by mounting express.raw({ type: "application/json" }) on the webhook route and passing req.body.toString("utf8") as rawBody.

Python

import hashlib, hmac, time

def verify(raw_body: bytes, headers, secret: str, tolerance: int = 300) -> bool:
    header = headers.get("X-Clarion-Signature-V2", "")
    parts = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)
    try:
        t = int(parts["t"])
        sig = parts["v1"]
    except (KeyError, ValueError):
        return False
    if abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(
        secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, sig)

Deduplicate retries

Clarion retries failed deliveries up to five times, and you can retry by hand from Recent deliveries. Every retry carries the same X-Clarion-Delivery id (and the same id in the body). Store the ids you’ve processed and ignore repeats — that’s what makes handling safe to run twice.

A retry is signed with a new timestamp, so a legitimate retry hours later still passes the 300-second check. createdAt in the body stays the time the event happened.

The deprecated header

Requests also carry X-Clarion-Signature: sha256=<hex>, an HMAC over the body alone. It has no timestamp, so a captured request signed this way stays valid forever. It’s still sent so older integrations keep working. Verify X-Clarion-Signature-V2 and stop reading the old header.

If verification fails

  • Check you’re hashing the raw bytes, not re-serialised JSON.
  • Check the secret belongs to this subscription — each webhook has its own.
  • Check your server clock. A clock more than five minutes off rejects every request.
08 — Get started

Ready to build unstoppable momentum?

Join teams that are aligned, accountable, and growing — with a business operating system built to be lived in every day.

Get started free Book a demo
No credit card required · Set up in minutes