CustomDomain Docs

Verifying signatures

Verify webhook deliveries with the X-JE-Signature HMAC-SHA256 header and the timestamp, and reject replayed requests using a recommended five minute window.

Each delivery carries these headers:

  • X-JE-Timestamp: <unix seconds>: when the delivery was signed.
  • X-JE-Signature: sha256=<hex>: an HMAC-SHA256, keyed by your endpoint's signing secret (whsec_…), over the string ${timestamp}.${rawBody} (the timestamp, a dot, then the exact bytes of the request body).
  • X-JE-Event-Id: evt_…: the event id, the same on every retry. Use it to deduplicate.

Binding the timestamp into the signature lets you reject replays: check that the timestamp is recent (a 5-minute window is recommended) before trusting the payload. Each attempt, including retries, is re-signed with a fresh timestamp.

const crypto = require('crypto');

function verify(rawBody, tsHeader, sigHeader, secret, toleranceSec = 300) {
  const ts = parseInt(tsHeader, 10);
  if (!Number.isFinite(ts)) return false;
  if (Math.abs(Date.now() / 1000 - ts) > toleranceSec) return false; // replay window
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(`${ts}.`)
    .update(rawBody)
    .digest('hex');
  const a = Buffer.from(sigHeader || ''), b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Compute the HMAC over the exact bytes received (don't re-serialize the JSON) and prepend ${timestamp}. before hashing. The comparison must be constant-time.

Send yourself a signed test delivery

To check a receiver before any real event reaches it, sign a body with your endpoint's secret the same way and post it to the receiver. Both samples read the secret from WEBHOOK_SECRET and send to a receiver at http://localhost:3000/webhooks/cd; change that to yours.

BODY='{"id":"evt_test","type":"connection.live","domain":"www.acme.test"}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -hex | sed 's/^.* //')
curl -X POST http://localhost:3000/webhooks/cd \
  -H "Content-Type: application/json" \
  -H "X-JE-Timestamp: $TS" \
  -H "X-JE-Signature: sha256=$SIG" \
  -H "X-JE-Event-Id: evt_test" \
  -d "$BODY"

Your receiver should answer 2xx for this request, and reject it once you change one byte of the body or send a timestamp older than your window. For real, signed deliveries without a real domain, register the endpoint on a test mode application.

On this page