View Categories

How Do You Verify a Webhook Signature?

1 min read

Why Verify #

Your endpoint is publicly reachable, so anyone can post to it. Verification proves that a payload originated from MixCalendar and was not altered in transit. Treat an unverified request as untrusted input.

The Signature Header #

Every delivery carries X-MixCalendar-Signature containing a timestamp and a hex digest:

X-MixCalendar-Signature: t=1756800862,v1=5f2c8a...

The digest is an HMAC-SHA256 over the string {timestamp}.{raw_body} using your endpoint’s signing secret.

Verifying in Node.js #

const crypto = require('crypto');

function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(',').map(p => p.split('='))
  );
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex');

  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1);
  if (a.length !== b.length) return false;
  if (!crypto.timingSafeEqual(a, b)) return false;

  const age = Math.abs(Date.now() / 1000 - Number(parts.t));
  return age < 300;
}

Use the Raw Body #

Compute the digest over the exact bytes received. Parsing JSON and re-serialising it changes key order and whitespace, which produces a different digest and a verification failure that is easy to misdiagnose. In Express, capture the raw body with express.raw() on the webhook route before any JSON parser runs.

Timestamp Tolerance #

Reject payloads older than about five minutes to limit replay attacks, while leaving enough room for normal clock drift between servers.

Rotating the Secret #

Rotation issues a new secret while the previous one stays valid for twenty-four hours, so accept either during that window and then drop the old one.

Powered by BetterDocs

Leave a Reply

Your email address will not be published. Required fields are marked *

Scroll to Top