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
- On the endpoint’s row in Settings → Integrations → Webhooks, click Roll secret.
- A new secret appears once — copy it immediately. The old secret stops working the moment you roll.
- 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
- Wrong secret — you rolled the secret but your receiver still holds the old one.
- Body was modified — a proxy or framework re-serialized the JSON. Hash the raw bytes instead.
- Stale timestamp — the delivery is a late retry beyond your replay window. Widen the window slightly, or rely on
X-ES-Deliveryto dedupe.
Related
- Set up webhooks
- See webhook deliveries and retry failures
- Test your integration in the Sandbox — includes a signature-verification checker