Guide to Facebook Webhooks: Features and Best Practices
Facebook webhooks let your app react to activity on the Graph API in real time: a new comment on a Page, an incoming Messenger message, a lead submitted through a lead ad. Meta pushes each change to your endpoint as a JSON POST instead of making you poll the Graph API for it.
This guide covers how Facebook webhooks work, the events you can subscribe to, the two-step setup (a verification handshake followed by signed deliveries), the limitations worth planning around, and the best practices that keep a Messenger, Pages, or lead-gen integration reliable in production.
What are Facebook webhooks?
Facebook webhooks are HTTP callbacks delivered through the Graph API's webhooks product. You subscribe an app to changes on an object (such as page, user, or permissions) and to specific fields on that object (such as feed or messages). When a subscribed field changes, Meta sends a POST to your configured callback URL with a JSON body describing what happened.
Facebook uses its own X-Hub-Signature-256 header for authenticity and a hub.challenge GET handshake to verify your endpoint at registration.
Facebook webhook features
| Feature | Details |
|---|---|
| Webhook configuration | App Dashboard (Webhooks product) or Graph API subscriptions |
| Endpoint verification | GET handshake echoing hub.challenge |
| Signature header | X-Hub-Signature-256 (SHA-256); legacy X-Hub-Signature is SHA-1 |
| Signature scheme | sha256= + hex HMAC-SHA256 of the raw body, keyed with the App Secret |
| Replay protection | None; signature carries no timestamp |
| Payload shape | {object, entry[]}; up to 1,000 updates batched per POST |
| Event model | (object, field) subscriptions, not dotted event names |
| Retry behavior | Immediate retry, then decreasing frequency over 36 hours, then dropped |
| Optional mTLS | Meta client cert, CN client.webhooks.fbclientcerts.com |
| SDKs | facebook-nodejs-business-sdk, facebook-business (no HMAC helper) |
Common events
Facebook webhook events are (object, field) pairs rather than dotted event names. You subscribe an app to an object, then enable the fields you care about. The page object is the most common for developer integrations:
| Object.field | Fires when |
|---|---|
page.feed | A post, comment, reaction, or share happens on a Page you manage |
page.messages | A user sends your Page a Messenger message |
page.messaging_postbacks | A user taps a postback button in a Messenger conversation |
page.leadgen | A user submits a Facebook lead ad form |
page.mention | Your Page is mentioned in a post or comment |
user.feed | A subscribed user's own feed changes |
permissions | A user grants or revokes a permission your app requested |
Each field's exact payload shape is documented on the Page and User webhooks reference. Enable only the fields you process, and consult Meta's reference for the authoritative, current list.
Setting up Facebook webhooks
Verify your endpoint (GET handshake)
When you add a callback URL in the App Dashboard (or via the Graph API), Meta sends a GET request with three query parameters: hub.mode (always subscribe), hub.verify_token (the Verify Token you set in the dashboard), and hub.challenge. Confirm the verify token matches, then echo the hub.challenge value back with a 200.
const VERIFY_TOKEN = process.env.FB_VERIFY_TOKEN;
app.get("/webhook", (req, res) => {
const mode = req.query["hub.mode"];
const token = req.query["hub.verify_token"];
const challenge = req.query["hub.challenge"];
if (mode === "subscribe" && token === VERIFY_TOKEN) {
return res.status(200).send(challenge);
}
res.sendStatus(403);
});
Subscribe to fields
Configure object and field subscriptions in the App Dashboard's Webhooks product, or via the Graph API. To receive a Page's events you also need pages_manage_metadata and must subscribe the app to that Page:
curl -X POST "https://graph.facebook.com/v21.0/{page-id}/subscribed_apps" \
-H "Content-Type: application/json" \
-d '{"subscribed_fields": "feed,messages,leadgen", "access_token": "{page-access-token}"}'
Apps in Development mode receive only test notifications; you need a Live app for real Page traffic.
Securing Facebook webhooks
Every POST carries an X-Hub-Signature-256 header: the string sha256= followed by a hex-encoded HMAC-SHA256 of the raw request body, keyed with your app's App Secret. (A legacy X-Hub-Signature header carries the SHA-1 equivalent; prefer the SHA-256 header.)
The critical detail: compute the HMAC over the exact raw bytes Meta sent. Meta signs an escaped-unicode form of the payload, so a body you've parsed and re-serialized will not match. Capture the raw body before any JSON middleware touches it.
const crypto = require("crypto");
const APP_SECRET = process.env.FB_APP_SECRET;
function verifySignature(rawBody, signatureHeader) {
const expected =
"sha256=" +
crypto.createHmac("sha256", APP_SECRET).update(rawBody).digest("hex");
const received = Buffer.from(signatureHeader || "");
const computed = Buffer.from(expected);
return (
received.length === computed.length &&
crypto.timingSafeEqual(received, computed)
);
}
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
if (!verifySignature(req.body, req.headers["x-hub-signature-256"])) {
return res.sendStatus(403);
}
const payload = JSON.parse(req.body);
res.sendStatus(200); // acknowledge fast
for (const entry of payload.entry) {
processQueue.add(entry); // process each entry asynchronously
}
});
The same check in Python (Flask):
import hashlib
import hmac
import os
from flask import Flask, request
app = Flask(__name__)
APP_SECRET = os.environ["FB_APP_SECRET"].encode()
def verify_signature(raw_body: bytes, signature_header: str) -> bool:
expected = "sha256=" + hmac.new(APP_SECRET, raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_header or "")
@app.post("/webhook")
def webhook():
if not verify_signature(request.get_data(), request.headers.get("X-Hub-Signature-256")):
return "", 403
payload = request.get_json()
for entry in payload["entry"]:
pass # enqueue each entry for async processing
return "", 200
For an extra layer, Meta can present a client certificate (mTLS) with the common name client.webhooks.fbclientcerts.com, which you can pin at your load balancer.
Facebook webhook limitations and pain points
Payloads are batched, and each POST holds many updates
The Problem: A single POST can carry up to 1,000 updates in its entry array, spanning multiple objects and fields. A handler that assumes one event per request will silently drop everything after the first.
Why It Happens: Meta batches changes for delivery efficiency, especially for high-traffic Pages.
Workarounds:
- Iterate the full
entryarray and process each change independently. - Enqueue each entry as its own unit of work so one bad entry doesn't fail the batch.
- Keep the synchronous handler to verification and enqueue only.
How Hookdeck Can Help: Hookdeck can split a batched payload into individual events with a transformation, so your application receives one clean event per delivery instead of unpacking arrays itself.
The signature has no timestamp, so no replay protection
The Problem: X-Hub-Signature-256 signs the body only. A captured, validly signed payload can be replayed and it will still verify. Signature verification alone doesn't prove freshness.
Why It Happens: Meta's scheme predates the timestamped designs that bake a replay window into the signature.
Workarounds:
- Make processing idempotent so a replayed delivery has no additional effect.
- Dedupe on identifiers inside the payload (message IDs, change IDs) rather than trusting each POST as unique.
How Hookdeck Can Help: Hookdeck deduplicates deliveries at the edge with configurable rules, filtering replays and retries before they reach your app. See our guide to webhook idempotency.
Deliveries expire after 36 hours
The Problem: If your endpoint is down or returning errors, Meta retries immediately, then at decreasing frequency for about 36 hours, after which the notification is dropped for good. There's no delivery log to recover it from.
Why It Happens: Meta bounds how long it will keep retrying a failing endpoint.
Workarounds:
- Acknowledge with a 200 fast and keep the endpoint highly available.
- Reconcile against the Graph API for anything you can't afford to miss.
- Log every raw delivery on receipt for your own audit trail.
How Hookdeck Can Help: Hookdeck accepts and acknowledges deliveries immediately, then retries to your downstream on a schedule you control and preserves failed events for replay, so a short outage on your side no longer means lost activity.
Raw-body handling is easy to break
The Problem: Meta signs an escaped-unicode form of the payload. If any middleware parses and re-serializes the body before you verify, the recomputed HMAC won't match and every delivery looks forged.
Why It Happens: Most web frameworks parse JSON automatically, discarding the exact bytes the signature was computed over.
Workarounds:
- Capture the raw body (
express.raw,request.get_data()) and verify before parsing. - Never re-serialize the payload prior to the signature check.
How Hookdeck Can Help: Hookdeck verifies the X-Hub-Signature-256 signature at the edge against the bytes as received, so your application receives already-verified events and never has to wrangle raw-body middleware.
Best practices
Verify against the raw body with the App Secret
Recompute sha256= + hex HMAC-SHA256 over the untouched bytes, keyed with the App Secret, and compare with a constant-time comparison. Prefer X-Hub-Signature-256 over the SHA-1 X-Hub-Signature.
Handle the GET handshake and POST deliveries separately
The GET verifies the endpoint via hub.challenge; the POST delivers signed events. Implement and test both, and keep the verify token secret.
Acknowledge fast, process asynchronously
Return 200 quickly and defer real work to a queue. Meta expects a prompt acknowledgement, and slow handlers turn into retries. See why to process webhooks asynchronously.
Process every entry in the batch
Loop the entry array and treat each change as its own unit. Don't assume one event per request.
Dedupe and reconcile
With no replay window and a 36-hour delivery ceiling, dedupe on payload identifiers and reconcile against the Graph API for critical data. Treat webhooks as notifications, not a system of record.
Make Facebook webhooks production-ready
Hookdeck verifies X-Hub-Signature-256, splits batched payloads, deduplicates, and durably queues every event
Conclusion
Facebook webhooks are how Pages, Messenger, and lead-gen activity reach your app without polling the Graph API. The mechanics are specific: an object-and-field subscription model, a hub.challenge GET handshake, and X-Hub-Signature-256 deliveries you have to verify against the exact raw bytes Meta signed. The delivery guarantees are modest too: batched payloads, no replay protection, and a 36-hour ceiling before events are dropped.
That leaves the operational work to you. Verifying signatures against the raw body, unpacking batches, deduplicating, acknowledging fast, and reconciling for the data you can't miss are all your responsibility unless you offload them.
For teams that would rather build on Facebook's activity than manage its delivery mechanics, Hookdeck verifies signatures, splits batches, deduplicates, and durably queues every event at the edge, so your app only ever sees clean, verified changes.
Get started with Hookdeck for free and handle Facebook webhooks reliably in minutes.