Delivery behaviour
Read from the vendor's own documentation, last checked 2026-08-06. Where the vendor states no number, this table carries none.
| Response budget | not published by the vendor |
|---|---|
| Retry | Retried with decreasing frequency over 36 hours. Unacknowledged notifications are dropped after that |
| Gives up | not published by the vendor |
| Source | https://developers.facebook.com/docs/graph-api/webhooks/getting-started |
Signature
Verified against a working verifier proven by a test suite, not read from documentation. Last verified 2026-08-20.
| Algorithm | HMAC-SHA256 |
|---|---|
| Signed payload | raw body |
| Encoding | hex |
| Header | x-hub-signature-256 = sha256=hex |
| Tolerance | none enforced |
Verify it
The raw request body, byte for byte, before any JSON parsing. Every scheme on this page breaks the moment a framework re-serializes the payload.
const crypto = require("node:crypto");
// Same scheme as GitHub: "sha256=" + hex HMAC of the raw body, keyed by the
// app secret.
const expected =
"sha256=" +
crypto.createHmac("sha256", appSecret).update(rawBody).digest("hex");
const valid = crypto.timingSafeEqual(
Buffer.from(req.headers["x-hub-signature-256"]),
Buffer.from(expected),
);Before any of this runs, Meta must accept the callback URL: it sends a GET carrying hub.mode=subscribe and expects hub.challenge echoed back verbatim. No echo, no subscription.
What bites
Meta publishes the retry window (36 hours, decreasing frequency, then the notification is dropped) but not a per-request response budget or a disable threshold. Where the vendor states no number, this page carries none.
Read more
AnyHook sits in front of endpoints that receive from Meta: it answers inside the budget above, retries on its own schedule when your server is down, and keeps every event replayable. Change one URL, keep your code.