A webhook is the fast path, not the guarantee. Notes on the three layers it takes before you can honestly tell a customer that no lead goes missing.
Meta's Lead Ads webhook is genuinely good: a form is submitted and the payload is at your endpoint in well under a second. The problem is not latency. The problem is that when a webhook stops arriving, nothing happens — no error, no alert, no gap you can see. Your dashboard simply shows a quiet afternoon, and the advertiser finds out from a customer who says they filled the form yesterday.
Everything below is one answer to that: three layers with genuinely independent failure modes, so no single thing going wrong loses a lead.
The instinct is to acknowledge Meta quickly and process afterwards. Half right. The ordering that matters is:
200.If step 2 fails, return 500 and let Meta retry — their retry
window is narrow, but it is the only second chance on offer. If the process dies
during step 4, the stored row is still sitting there unprocessed and a replay
tool can re-run it. Acking before persisting inverts this: you have promised Meta
the lead is safe while it exists only in memory.
Meta re-sends the same payload with the same X-Hub-Signature-256.
That header is a perfectly good idempotency key: put a unique index on it and let
the duplicate insert fail. A failed insert means the original delivery is already
done or in flight, so there is nothing to do.
Two details worth stealing. Deduplicating on a hash you compute over
the body is more code and more ways to be subtly wrong. And in MySQL a unique
index permits multiple NULLs, so unsigned test payloads still flow
through instead of colliding with each other.
Webhook delivery stops for reasons that never produce an error on your side: an app-level subscription quietly drops, a page token expires, an app-secret rotation makes every HMAC mismatch, or Meta has a bad hour. Each of those looks identical to "nobody filled in the form".
So a second path re-asks the Graph API per form on a cadence and saves anything the webhook did not. A few things this taught us:
(leadgen_id, user_id) means the two paths can
race freely. Check before inserting as well, so a lead that both paths find
is never dispatched twice.This is the failure that motivated the third layer, and it is not obvious until it happens.
After a platform incident, data does not simply resume — it arrives late, carrying its original timestamps. Your poller, meanwhile, kept running and kept advancing its cursor across a window that was empty at the time. When the backfill lands behind the cursor, nothing ever looks there again. The leads exist in Meta's API and will never exist in yours, and no error was raised at any point.
Once a day, re-ask for a wide window across every active form and insert whatever is missing. No cursor, no cleverness. It is not a feature and it has no flag to forget to switch on; it is the standing guarantee that a lead cannot stay invisible just because a cursor moved past it.
Pick the hour from your own traffic, not from habit. Ours runs at 03:00 Tashkent time, chosen off the hourly lead histogram: roughly 80 leads an hour at 03:00 against roughly 800 at peak. The extra Graph calls then compete with almost nothing.
| Layer | Catches | Misses |
|---|---|---|
| Webhook | Everything, in under a second, on a normal day. | Anything that silently stops delivery. |
| Polling | Subscription drift, token expiry, HMAC mismatch, short outages. | Backfill that lands behind an advanced cursor. |
| Nightly reconcile | Everything the other two lost, within a day. | Nothing — but it is slow, so it is a net, not a path. |
The layers are only worth the code if their failure modes are independent. Three retries of the same request are one layer wearing a disguise.
Receiving it reliably is half the job; the other half is getting it to the place the operator is actually looking, and knowing whether that worked. The judgement rules for the outbound side — success conditions, and permanent versus retryable rejections — are on the custom destinations page. If your form lives on your own landing page rather than on Meta, the inbound webhook reference covers that route in.
All of this runs in production at Targenix — free Facebook and Instagram lead automation for Uzbekistan, routing to 50+ destinations (the list) at no cost.