How to integrate Meta Lead Ads with your CRM: a complete setup guide
Connecting Meta Lead Ads to a CRM looks straightforward until you hit the webhook subscription step nobody documents. This guide covers every pitfall in order.

Getting leads from Meta Lead Ads into a CRM sounds straightforward: Meta fires a webhook, you fetch the lead, you store it. In practice there are at least four separate places where the setup silently breaks, each with a misleading error message.
This guide covers the complete integration from the Meta side — app setup, Business Manager configuration, webhook implementation, and the page subscription step most tutorials skip. The Meta setup steps are identical regardless of what language or framework you use. Code examples are shown in Node.js, PHP, and Python. You can find the official Meta reference at developers.facebook.com/docs/marketing-api/guides/lead-ads.
How the data flow actually works
Before touching any code, understand the four-step flow:
- A user submits the form inside Facebook or Instagram
- Meta sends a
POSTto your server with aleadgen_id— not the actual lead data - Your server calls the Graph API with that ID to fetch the full lead (name, phone, email, custom fields)
- You write the lead to your database
The webhook payload is a pointer, not the data itself. Your webhook handler needs two credentials: the app secret (to verify incoming request signatures) and an access token (to fetch the full lead from Graph API). Both come from the Meta setup steps below.
Meta App and Business Manager setup
These steps happen entirely in the Meta dashboard — no code involved yet.
The app must be in Live mode. In Development mode, only test users can submit leads. Real Facebook users' form submissions do not trigger your webhook at all. Go to App Settings > Basic, toggle from Development to Live. You need a privacy policy URL and app icon before Meta will let you publish.
Add the "Capture & manage ad leads" use case. In your app dashboard go to Use Cases, add "Capture & manage ad leads with Marketing API," then click Customize. This unlocks the leads_retrieval and pages_manage_metadata permissions. Without this, the permissions dropdown when generating a system user token will be empty.
Create a System User with the right assignment. In Business Manager go to Business Settings > System Users. Create a system user with Admin role. Then go to Business Settings > Apps, find your app, and click Add People — but use the System Users tab, not the People tab. Wrong tab means no permissions on the generated token.
Add your Facebook Page as an asset with Full Control. In Business Settings > System Users, click your system user, then Add Assets > Pages. Assign with Full Control. Without this, GET /{page-id}?fields=access_token returns "Object does not exist" — which actually means token lacks page access, not that the page is missing.
Generate the system user token after the page is added. Tokens only capture permissions for resources assigned at the time of generation. If you generated a token before adding the page, regenerate it. Select: leads_retrieval, pages_manage_metadata, pages_show_list, ads_management.
Webhook implementation (Node.js, PHP, Python)
Your webhook endpoint needs a GET handler for Meta's verification handshake, and a POST handler for incoming leads.
GET — verification handshake
Meta sends a GET with hub.mode, hub.verify_token, and hub.challenge when you register your webhook. Echo the challenge back if the token matches.
Node.js (Express):
app.get('/webhooks/meta-leads', (req, res) => {
const { 'hub.mode': mode, 'hub.verify_token': token, 'hub.challenge': challenge } = req.query;
if (mode === 'subscribe' && token === process.env.META_VERIFY_TOKEN) {
return res.status(200).send(challenge);
}
res.sendStatus(403);
});
PHP (Laravel):
Route::get('/webhooks/meta-leads', function (Request $request) {
if ($request->query('hub_mode') === 'subscribe'
&& $request->query('hub_verify_token') === env('META_VERIFY_TOKEN')) {
return response($request->query('hub_challenge'), 200);
}
return response('Forbidden', 403);
});
Python (Flask):
@app.route('/webhooks/meta-leads', methods=['GET'])
def verify():
if request.args.get('hub.mode') == 'subscribe' and \
request.args.get('hub.verify_token') == os.environ['META_VERIFY_TOKEN']:
return request.args.get('hub.challenge'), 200
return 'Forbidden', 403
POST — receive and verify
Always verify the HMAC-SHA256 signature in x-hub-signature-256 before processing. Compare with a timing-safe equality check to prevent timing attacks.
expected = "sha256=" + HMAC_SHA256(app_secret, raw_request_body).hex()
if not timing_safe_equal(expected, received_signature):
return 403
Read the raw request body before parsing JSON so the HMAC can be computed on the unmodified bytes. After verification, parse the body to extract leadgen_id, then call GET https://graph.facebook.com/v19.0/{leadgen_id}?access_token={META_ACCESS_TOKEN} for the full lead data.
Parsing form fields
Meta lead forms are inconsistent. full_name, fullname, and first_name + last_name all appear in the wild. Always trim field names before matching — Meta occasionally sends trailing whitespace, causing exact-match misses even when the field is present. For custom fields, limit matching to names shorter than 35 characters to avoid accidentally matching the text of a long question like "are_you_seriously_planning_to_study_abroad_for_phd?".
Store the complete raw field_data alongside parsed values. Every form has different questions and you will lose data you didn't account for. The raw data also serves as a dedup key — check for an existing lead with the same leadgen_id before creating a new record to handle Meta's webhook retries gracefully.
The subscription step most integrations miss
Registering the webhook URL in Meta's app dashboard is not enough. You must separately subscribe your Facebook Page to your app's webhook. Without this, leads appear in Meta's Lead Center but nothing reaches your server.
POST /{page-id}/subscribed_apps?subscribed_fields=leadgen&access_token={PAGE_TOKEN}
This requires a page access token, not a system user token. Using the wrong token type returns error #210. Get a page token with:
GET /{page-id}?fields=access_token&access_token={SYSTEM_USER_TOKEN}
Copy the access_token from that response and use it in the subscription call. A successful response is { "success": true }. Confirm the subscription with GET /{page-id}/subscribed_apps — your app should appear with leadgen in the subscribed fields.
This subscription persists. You do not need to redo it unless you delete the webhook configuration. If GET /me/accounts returns an empty array, that is expected when your page is managed through Business Manager rather than a personal profile. Use the system user token with a direct page ID lookup instead.
Frequently asked questions
Why does the webhook Test button give "Object does not exist" in my server logs?
The Test button in the Webhooks UI sends a synthetic leadgen_id that does not exist in Graph API. Your server correctly receives the webhook, tries to fetch the lead, and gets a valid "not found" response. Use the Lead Ads Testing Tool for real end-to-end tests — submit a test form there and the full pipeline runs with a real leadgen ID.
Do I need to redo the page subscription if I change my webhook URL?
No, unless you delete the webhook configuration entirely. Updating the callback URL preserves existing subscriptions. You do need to re-verify the new URL, but the page subscription stays intact.
Where can I find the official Meta documentation for this integration?
The full reference is at developers.facebook.com/docs/marketing-api/guides/lead-ads. The retrieving leads section covers Graph API calls and field formats. The Graph API Explorer is useful for testing token exchanges and subscription calls without writing code.
If you're building a lead pipeline or CRM integration and want help with the implementation, talk to us.