Webhooks / Direct API + Meta Ads Setup Guide
Most webhooks to Meta Ads setups I audit are missing critical data points or sending malformed events. About 60% have attribution breaking silently because they’re not passing the required fbp and fbc parameters correctly, and another 30% are losing mobile app events entirely because they’re not handling the Conversions API payload structure properly.
This guide walks you through building a bulletproof webhook-to-Meta setup that actually attributes conversions correctly.
What You’ll Have Working By The End
- Webhook endpoint that receives form submissions and fires Meta Conversions API events in real-time
- Proper browser parameter collection (
fbp,fbc,user_agent) for attribution - Server-side event deduplication with client-side pixel events
- Mobile app event handling via webhook payload
- Complete event verification in Meta Events Manager
Prerequisites
- Meta Business Manager access with Conversions API permissions
- Meta Pixel installed on your forms (for deduplication)
- Server environment that can receive HTTP POST requests (Cloud Run, AWS Lambda, or dedicated server)
- Form system that can send webhook payloads on submission
- Meta access token with
ads_managementandbusiness_managementpermissions
Step 1: Set Up Meta Conversions API Access
First, get your API credentials sorted. In Meta Business Manager, go to Events Manager → Data Sources → your pixel → Settings → Conversions API.
Generate an access token with these permissions:
ads_managementbusiness_management
You’ll need three key values:
- Pixel ID (13-15 digit number)
- Access Token (starts with
EAA...) - Test Event Code (for verification, looks like
TEST12345)
Save these in your environment variables:
META_PIXEL_ID=123456789012345
META_ACCESS_TOKEN=EAAxxxxxxxxx
META_TEST_EVENT_CODE=TEST12345
Step 2: Build Your Webhook Endpoint
Here’s a production-ready webhook endpoint that handles Meta Conversions API events. I’m using Node.js because it’s what I see working most reliably in client setups:
const express = require('express');
const crypto = require('crypto');
const axios = require('axios');
const app = express();
app.use(express.json());
// Meta Conversions API endpoint
const META_API_URL = `https://graph.facebook.com/v19.0/${process.env.META_PIXEL_ID}/events`;
app.post('/webhook/meta-conversion', async (req, res) => {
try {
const {
email,
phone,
first_name,
last_name,
event_name = 'Lead',
fbp,
fbc,
user_agent,
ip_address,
event_source_url,
value,
currency = 'USD'
} = req.body;
// Hash PII data
const hashedEmail = email ? crypto.createHash('sha256').update(email.toLowerCase().trim()).digest('hex') : null;
const hashedPhone = phone ? crypto.createHash('sha256').update(phone.replace(/\D/g, '')).digest('hex') : null;
const hashedFirstName = first_name ? crypto.createHash('sha256').update(first_name.toLowerCase().trim()).digest('hex') : null;
const hashedLastName = last_name ? crypto.createHash('sha256').update(last_name.toLowerCase().trim()).digest('hex') : null;
// Build event payload
const eventData = {
data: [{
event_name: event_name,
event_time: Math.floor(Date.now() / 1000),
action_source: 'website',
event_source_url: event_source_url,
user_data: {
em: hashedEmail ? [hashedEmail] : undefined,
ph: hashedPhone ? [hashedPhone] : undefined,
fn: hashedFirstName ? [hashedFirstName] : undefined,
ln: hashedLastName ? [hashedLastName] : undefined,
client_ip_address: ip_address,
client_user_agent: user_agent,
fbp: fbp,
fbc: fbc
},
custom_data: value ? {
value: parseFloat(value),
currency: currency
} : undefined,
event_id: `${hashedEmail || 'anon'}_${Date.now()}` // For deduplication
}],
test_event_code: process.env.META_TEST_EVENT_CODE // Remove in production
};
// Remove undefined fields
eventData.data[0].user_data = Object.fromEntries(
Object.entries(eventData.data[0].user_data).filter(([_, v]) => v != null)
);
// Send to Meta
const response = await axios.post(META_API_URL, eventData, {
headers: {
'Authorization': `Bearer ${process.env.META_ACCESS_TOKEN}`,
'Content-Type': 'application/json'
}
});
console.log('Meta API Response:', response.data);
res.status(200).json({ success: true, meta_response: response.data });
} catch (error) {
console.error('Webhook error:', error.response?.data || error.message);
res.status(500).json({ error: 'Failed to process webhook' });
}
});
app.listen(3000, () => {
console.log('Webhook server running on port 3000');
});
Deploy this to Cloud Run, AWS Lambda, or your preferred hosting. You’ll need a public HTTPS endpoint.
Step 3: Configure Browser Data Collection
Meta attribution requires browser parameters that most webhook setups miss. Add this JavaScript to your forms to collect the required data:
// Add this to your form pages
function collectMetaParams() {
// Get fbp from Meta pixel cookie
const fbp = getCookie('_fbp');
// Get fbc from URL parameter (if available)
const urlParams = new URLSearchParams(window.location.search);
const fbc = urlParams.get('fbclid') ? `fb.1.${Date.now()}.${urlParams.get('fbclid')}` : getCookie('_fbc');
return {
fbp: fbp,
fbc: fbc,
user_agent: navigator.userAgent,
ip_address: null, // Server will detect this
event_source_url: window.location.href
};
}
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return null;
}
// Include this data in your form submission webhook payload
document.getElementById('your-form').addEventListener('submit', function(e) {
const metaParams = collectMetaParams();
// Add to your webhook payload
const webhookData = {
email: document.getElementById('email').value,
first_name: document.getElementById('first_name').value,
last_name: document.getElementById('last_name').value,
phone: document.getElementById('phone').value,
event_name: 'Lead',
...metaParams
};
// Send to your webhook endpoint
fetch('/webhook/meta-conversion', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(webhookData)
});
});
Step 4: Set Up Event Deduplication
If you’re also using Meta Pixel on the client-side (which you should), you need to deduplicate events to avoid double-counting. Here’s how to sync the event IDs:
// Client-side pixel event with matching event_id
function fireMetaPixelEvent(email, eventName = 'Lead') {
const eventId = `${email || 'anon'}_${Date.now()}`;
// Fire pixel event
fbq('track', eventName, {}, { eventID: eventId });
// Return the same event ID for your webhook
return eventId;
}
// Use in your form submission
document.getElementById('your-form').addEventListener('submit', function(e) {
const email = document.getElementById('email').value;
const eventId = fireMetaPixelEvent(email, 'Lead');
const webhookData = {
email: email,
// ... other fields
event_id: eventId // This prevents double-counting
};
fetch('/webhook/meta-conversion', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(webhookData)
});
});
Step 5: Handle Mobile App Events
If your webhook receives events from mobile apps, the payload structure is different. Here’s how to handle both web and app events:
// Add this to your webhook endpoint after the web event handling
if (req.body.action_source === 'app') {
const appEventData = {
data: [{
event_name: event_name,
event_time: Math.floor(Date.now() / 1000),
action_source: 'app',
app_data: {
application_tracking_enabled: true,
advertiser_tracking_enabled: true,
consider_views: true
},
user_data: {
em: hashedEmail ? [hashedEmail] : undefined,
ph: hashedPhone ? [hashedPhone] : undefined,
fn: hashedFirstName ? [hashedFirstName] : undefined,
ln: hashedLastName ? [hashedLastName] : undefined,
madid: req.body.mobile_ad_id, // Mobile Advertising ID
anon_id: req.body.anonymous_id
},
custom_data: value ? {
value: parseFloat(value),
currency: currency
} : undefined,
event_id: req.body.event_id || `${hashedEmail || 'anon'}_${Date.now()}`
}],
test_event_code: process.env.META_TEST_EVENT_CODE
};
}
Testing & Verification
1. Test Event Code Verification
With the test event code in your payload, go to Meta Events Manager → Test Events. You should see your webhook events appearing in real-time with this structure:
- Event Name: Lead (or your specified event)
- Action Source: website
- Event Time: Within 1-2 seconds of submission
- Match Quality: Should show email, phone, and other matched parameters
2. Debug Mode Check
In Events Manager, enable “Debug Mode” for your pixel. Submit a test form and verify:
- Server event appears with “CAPI” label
- Client-side event (if applicable) appears with “Browser” label
- Same
event_idshows both events are deduplicated - All user data fields show as “Matched”
3. Attribution Verification
Check your Meta Ads reporting after 24-48 hours. Look for:
- Conversion events appearing in Ads Manager
- Attribution matching your expected customer journey
- No significant drop in conversion volume compared to pixel-only tracking
Acceptable variance: 5-10% difference between server-side and client-side event counts is normal due to ad blockers and browser restrictions.
Troubleshooting
Problem → Solution
Events showing in Test Events but not attributing in Ads Manager
→ Check that your webhook is passing fbp and fbc parameters correctly. These are required for attribution. Also verify your access token has ads_management permissions.
“Invalid parameter” errors in Meta API response
→ Make sure you’re hashing PII data with SHA256 and removing all special characters from phone numbers before hashing. Email should be lowercased and trimmed.
Duplicate events showing in reporting
→ Your event_id isn’t consistent between client-side pixel and server-side API calls. Use the same ID generation logic on both sides.
Mobile app events not attributing
→ Verify you’re including madid (Mobile Advertising ID) in the user_data object and setting action_source: 'app'. App events require different parameters than web events.
High match rate in Test Events but low conversion attribution
→ Your webhook might be firing too late. Meta prefers events within 7 days of ad click, ideally within 1-2 hours. Check your webhook processing delay.
403 “Insufficient permissions” errors
→ Regenerate your access token in Business Manager with explicit ads_management and business_management permissions. System-generated tokens sometimes lack required scopes.
What To Do Next
Ready to expand your server-side tracking? Check out these related setups:
- Webhooks Overview — Compare webhook tracking vs other server-side methods
- GTM Server-Side for Meta Ads — Alternative container-based approach
- Contact Luc for a free tracking audit if you’re seeing attribution issues
This guide is part of the Webhooks Tracking Hub — comprehensive guides for setting up direct API integrations with major ad platforms.