Need to manage API keys, view logs, or check usage?Open the Developer portal →

Verifying signatures

Every webhook delivery includes a Circa-Signature header. Verify it before trusting the payload.

The header format

Circa-Signature: t=1747000800,v1=8e4dbf…
  • t — unix-seconds timestamp at the moment Circa computed the signature.
  • v1 — hex-encoded HMAC-SHA256 of the string {t}.{rawBody}using your endpoint's signing secret as the key, where rawBody is the bytes you received in the HTTP body (not a re-serialized version of the parsed JSON).
Compute the HMAC over the raw request body. Re-encoding the parsed JSON will change whitespace and key order and the signature will no longer match.

Verification algorithm

  1. Read the Circa-Signature header.
  2. Split on , into t=… and v1=… components.
  3. Reject if |now - t| > 300 seconds (5-minute replay tolerance).
  4. Compute expected = hex(hmac_sha256(secret, "{t}.{rawBody}")).
  5. Constant-time compare expected with v1. Accept if equal, reject otherwise.

Snippets

import crypto from 'node:crypto';
import express from 'express';

const app = express();

// Capture the RAW body — don't let express.json() rewrite it.
app.use(express.raw({ type: 'application/json' }));

function verifyCircaSignature(
  header: string,
  rawBody: Buffer,
  secret: string,
  toleranceSeconds = 300,
) {
  const parts = Object.fromEntries(
    header.split(',').map((kv) => {
      const i = kv.indexOf('=');
      return [kv.slice(0, i).trim(), kv.slice(i + 1).trim()];
    }),
  );
  const t = Number(parts.t);
  const v1 = parts.v1;
  if (!Number.isFinite(t) || typeof v1 !== 'string') return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSeconds) return false;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${t}.${rawBody.toString('utf8')}`)
    .digest('hex');
  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(v1, 'hex');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post('/webhooks/circa', (req, res) => {
  const header = req.header('Circa-Signature') ?? '';
  if (!verifyCircaSignature(header, req.body, process.env.CIRCA_WHSEC!)) {
    return res.status(400).send('invalid signature');
  }
  const event = JSON.parse(req.body.toString('utf8'));
  // … handle event …
  res.status(200).send('ok');
});