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.