Verify webhook signatures

Every webhook delivery is signed so you can prove it came from Essential Support and was not tampered with in transit. Verify the X-ES-Signature header against your endpoint’s signing secret before you trust the payload.

The signature header

The header has the form t=<unix>,v1=<hex>, where t is the Unix timestamp of signing and v1 is HMAC-SHA256 of the string ${t}.${rawBody} keyed with your endpoint’s signing secret. Sign over the raw request body — the exact bytes you received, before any JSON parsing or re-serialization.

X-ES-Signature: t=1754754000,v1=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08

Verify it (Node)

Recompute the HMAC over t and the raw body, compare in constant time, and reject deliveries whose timestamp is more than 300 seconds old to blunt replay:

import crypto from 'node:crypto';

// Express: mount express.raw({ type: 'application/json' }) on the webhook route
// so req.body is the untouched Buffer (raw bytes), not a parsed object.
function verify(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;   // stale (> 5 min)
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  return v1.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}

Key points: use the endpoint’s own secret (each endpoint has its own), compare with timingSafeEqual rather than ===, and always hash the raw bytes — if your framework parses JSON first, re-stringifying it will change whitespace and the signatures will never match.

Rotating the secret

  1. On the endpoint’s row in Settings → Integrations → Webhooks, click Roll secret.
  2. A new secret appears once — copy it immediately. The old secret stops working the moment you roll.
  3. Deploy your receiver with the new secret. Roll during a quiet window if you cannot deploy instantly, because in-flight deliveries signed with the old secret will fail verification until the new one is live.

If verification fails