Verify Meta’s X-Hub-Signature-256 in Node.js (correctly)
2026-10-09
Every webhook Meta sends carries X-Hub-Signature-256: sha256=<hex> — an
HMAC-SHA256 of the raw request body keyed with your app secret. Verify it
or anyone who finds your URL can inject fake leads.
The code
import { createHmac, timingSafeEqual } from 'node:crypto'; function verify(rawBody,
header, appSecret) { if (!header?.startsWith('sha256=')) return false; const expected =
createHmac('sha256', appSecret).update(rawBody).digest('hex'); const a =
Buffer.from(header.slice(7), 'hex'); const b = Buffer.from(expected, 'hex'); return
a.length === b.length && timingSafeEqual(a, b); }
The three classic mistakes
1. Parsed body. If you feed JSON.stringify(req.body) to the
HMAC it will almost never match — key order, unicode escaping and whitespace differ. Keep
the raw bytes (in Express: express.raw() or the verify hook;
capture before any JSON middleware). 2. String comparison.
=== leaks timing; use timingSafeEqual after checking lengths.
3. Wrong secret. The HMAC key is the app secret (App Dashboard →
Settings → Basic), not the verify token — the verify token is only for the one-time
hub.challenge GET handshake.
Also worth knowing
Respond 200 before doing slow work; Meta deactivates endpoints that keep failing. And if you forward events onward, downstream tools verifying "Meta's" signature need the body re-signed — the original header is only valid for the original bytes.
Relayo verifies every Meta signature on the raw body, and re-signs each forwarded copy so your tools’ own verification keeps passing.
Get early access to Relayo