Meta Conversions API (CAPI)
Datafly Signal delivers events to Meta (Facebook) server-to-server using the Conversions API (CAPI). This provides reliable event delivery that is not affected by ad blockers or browser tracking prevention.
Why route Meta CAPI through Datafly Signal
- Conversions survive ad blockers and tracking prevention. A browser tag served from an advertising domain is blocked outright for part of your traffic, and Safari’s ITP shortens the cookies it can set. Signal sends the event from your own server, so the conversions Meta CAPI optimises against are not filtered by the browser.
- Email, phone number, first name, last name, postcode, city, country and address are hashed before leaving your infrastructure. Signal normalises (trim and lowercase) and applies SHA-256 inside your own deployment, so Meta CAPI receives match keys rather than raw personal data.
- Client IP and user agent are carried in the payload. Both are taken from the original visitor request rather than your server, which is what Meta CAPI needs to attribute the event to the right session.
- Click identifiers are captured and replayed. Signal stores
fbc,fbpfirst-party at collection time and attaches them to the conversion later, so attribution survives even when the original landing cookie has expired. - Presets for Retail, Travel, B2B / SaaS and Automotive. The mapping starts from a working configuration for your vertical instead of a blank field map.
- One consent decision governs every destination. Meta CAPI is gated by the same consent state as the rest of your stack, so a withdrawal applies everywhere at once.
- No vendor tag on the page. Removing it removes its page weight and its independent access to your visitors.
Prerequisites
Before configuring Meta CAPI in Signal, you need a Meta Business account with an active Pixel and a system user access token. Follow the steps below to set everything up.
Step 1: Create a Meta Business Account
If you don’t already have one:
- Go to business.facebook.com and click Create Account.
- Enter your business name, your name, and business email.
- Complete the setup wizard.
Step 2: Create or Find Your Meta Pixel
- In Meta Business Suite, go to Events Manager (left sidebar, under Data Sources).
- If you already have a Pixel, click on it and note the Pixel ID (a numeric ID like
1234567890). - To create a new Pixel:
- Click Connect Data Sources > Web > Connect.
- Name your Pixel (e.g. “My Website Pixel”) and enter your website URL.
- Select Conversions API as the connection method.
- Click Create Pixel. Note the Pixel ID.
Step 3: Create a System User
System users are the recommended way to generate long-lived access tokens for server-to-server integrations. Personal access tokens expire and should not be used in production.
- Go to Business Settings > Users > System Users.
- Click Add to create a new system user.
- Give it a name (e.g. “Datafly Signal”) and set the role to Admin.
- Click Create System User.
Step 4: Assign Permissions to the System User
- On the system user you just created, click Add Assets.
- Select Pixels from the asset type list.
- Find your Pixel and toggle on Manage Pixel permission.
- Click Save Changes.
Step 5: Generate an Access Token
- Still on the system user page, click Generate New Token.
- Select the following permissions:
ads_managementbusiness_management
- Click Generate Token.
- Copy the token immediately — it will only be shown once.
Store this token securely. If you lose it, you will need to generate a new one. The token does not expire unless revoked.
Step 6: Verify Your Domain (Recommended)
Domain verification improves event attribution and is required for some features:
- Go to Business Settings > Brand Safety > Domains.
- Click Add and enter your domain.
- Choose a verification method (DNS TXT record is recommended).
- Complete verification.
Configure in Signal
Now that you have your Pixel ID and access token, configure the integration in Signal.
Configuration Fields
| Field | Required | Description |
|---|---|---|
pixel_id | Yes | Your Meta Pixel ID (numeric). Found in Meta Events Manager. |
access_token | Yes | System user access token with ads_management permission. |
test_event_code | No | Test event code for validation. Events sent with this code appear in the Events Manager Test Events tab but do not affect reporting. |
Management UI Setup
- Go to Integrations > Add Integration > Meta Conversions API.
- Choose a variant:
- Retail — full e-commerce funnel (product views, cart, checkout, purchase, refunds)
- Travel — search, booking, and cancellation events
- B2B / SaaS — sign-up, trial, subscription, and lead events
- Default — standard page view and conversion tracking
- Enter your
pixel_idandaccess_token. - Select consent categories (typically
advertisingormarketing). - Click Save.
Management API Setup
curl -X POST http://localhost:8084/v1/admin/integrations \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"source_id": "src_abc123",
"vendor": "meta_capi",
"name": "Meta CAPI Production",
"enabled": true,
"config": {
"pixel_id": "1234567890",
"access_token": "your_access_token"
},
"consent_categories": ["advertising"]
}'API Endpoint
POST https://graph.facebook.com/v25.0/{pixel_id}/events?access_token={access_token}Events are sent as a JSON array in the data field.
Identity signals
Meta matches a server event to a user with the signals below. The more a visitor carries, the higher the Event Match Quality (EMQ).
Sent automatically
| Signal | Field | Source |
|---|---|---|
| Browser ID | user_data.fbp | The _fbp value Signal sets first-party (fb.1.<timestamp>.<random>). |
| Click ID | user_data.fbc | Built from the fbclid of a Meta ad click (fb.1.<timestamp>.<fbclid>). |
| External ID | user_data.external_id | Your user ID, else Signal’s anonymous ID, as sent. |
| IP address | user_data.client_ip_address | The visitor’s request, not your server. |
| User agent | user_data.client_user_agent | The visitor’s request. |
Hashed personal data
Meta only matches on these values, so each is sent as a normalised SHA-256 hash, never in plain text:
| Field | What is hashed |
|---|---|
em | Email, trimmed and lower-cased |
ph | Phone, shaped to E.164 first |
fn, ln | First and last name, trimmed and lower-cased |
ct, st, zp, country | City, region, postcode and country from the address trait, trimmed and lower-cased |
Hashing happens in your own deployment before the event leaves it. See Personal data by integration.
How to send user data
Call datafly.identify() when a user logs in, registers or submits a form:
datafly.identify("user-123", {
email: "jane.doe@example.com",
phone: "+44 7700 900123",
first_name: "Jane",
last_name: "Doe",
address: { city: "London", region: "ENG", postal_code: "SW1A 1AA", country: "GB" }
});Presets and event mapping
Content IDs and contents (Meta’s shape: id, quantity, item_price) come from your items[] (canonical), else the legacy products[], else a single flat item_id / product_id (automotive: then the legacy vehicle_id). search_string is search_term, else the legacy query; order_id is transaction_id, else order_id; value is value (the total including tax and shipping, as the event specs define it), else the legacy revenue. Meta requires a value and a currency on Purchase: a purchase without a currency is not sent. To customise, edit the installed blueprint’s field mappings in the Management UI.
Retail preset (ecommerce)
Full retail funnel for the Meta Conversions API: PageView, Search (search_string), ViewContent, AddToWishlist, AddToCart, InitiateCheckout, AddPaymentInfo, Purchase (value and currency required, order ID) and Lead. content_ids and contents (id, quantity, item_price) come from items[], else the legacy products[], else a flat item. Every user_data field is a normalised SHA-256 hash.
| Signal event | Meta event | Fields the preset sets |
|---|---|---|
page | PageView | global fields only. |
search | Search | contents, content_ids, search_string, value, currency, content_type, content_category. |
view_item | ViewContent | contents, content_ids, value, currency, content_type, content_name, content_category. |
add_to_wishlist | AddToWishlist | contents, content_ids, value, currency, content_type, content_name, content_category. |
add_to_cart | AddToCart | contents, content_ids, value, currency, content_type, content_name. |
begin_checkout | InitiateCheckout | contents, content_ids, value, currency, num_items, content_type. |
add_payment_info | AddPaymentInfo | contents, content_ids, value, currency, content_type. |
purchase | Purchase | contents, content_ids, value, currency, order_id, num_items, content_type. Sent only when properties.currency is set. |
generate_lead | Lead | content_name, content_category, value, currency. |
Travel preset (travel)
Travel funnel: Search, ViewContent for listings, AddToWishlist, InitiateCheckout, AddPaymentInfo, Purchase for a completed booking, Lead, and booking_cancelled as the custom event BookingCancelled (not a lead). Content IDs from items[].
| Signal event | Meta event | Fields the preset sets |
|---|---|---|
page | PageView | global fields only. |
destination_searched | Search | contents, content_ids, search_string, value, currency, content_type, content_category. |
listing_viewed | ViewContent | contents, content_ids, value, currency, content_type, content_name, content_category. |
listing_saved | AddToWishlist | contents, content_ids, value, currency, content_type, content_name. |
booking_started | InitiateCheckout | contents, content_ids, value, currency, num_items, content_type. |
add_payment_info | AddPaymentInfo | contents, content_ids, value, currency, content_type. |
booking_completed | Purchase | contents, content_ids, value, currency, order_id, content_type. Sent only when properties.currency is set. |
booking_cancelled | BookingCancelled | order_id, value, currency. |
generate_lead | Lead | content_name, value, currency. |
B2B / SaaS preset (saas)
SaaS lifecycle for the Meta Conversions API: CompleteRegistration, Lead, StartTrial, Subscribe (value and currency required), SubmitApplication and Donate.
| Signal event | Meta event | Fields the preset sets |
|---|---|---|
page | PageView | global fields only. |
sign_up | CompleteRegistration | content_name, value, currency, status. |
generate_lead | Lead | content_name, content_category, value, currency. |
trial_started | StartTrial | value, currency, predicted_ltv. |
subscription_started | Subscribe | value, currency, predicted_ltv. |
application_submitted | SubmitApplication | global fields only. |
donation_made | Donate | value, currency. |
Automotive preset (automotive)
Vehicle marketing funnel: Search, ViewContent, AddToWishlist, Download, Lead, Schedule and Contact, with content IDs from items[] or the flat item_id (the legacy vehicle_id as a fallback).
| Signal event | Meta event | Fields the preset sets |
|---|---|---|
page | PageView | global fields only. |
vehicle_searched | Search | search_string, make, model, body_type, fuel_type, condition, search_results_count. |
vehicle_viewed | ViewContent | contents, content_ids, content_name, content_category, value, currency, condition, fuel_type, year, mileage, dealer_id, content_type. |
vehicle_compared | ViewContent | contents, content_ids, compared_count, content_type. |
vehicle_saved | AddToWishlist | contents, content_ids, content_name, content_category, value, currency, content_type. |
brochure_downloaded | Download | content_ids, make, model, content_type. |
finance_quote_requested | Lead | contents, content_ids, value, currency, finance_type, content_type. |
trade_in_valued | Lead | value, currency, make, model, year. |
test_drive_booked | Schedule | contents, content_ids, dealer_id, appointment_at, make, model, content_type. |
dealer_contacted | Contact | contents, content_ids, dealer_id, enquiry_type, channel, content_type. |
service_appointment_booked | Schedule | contents, content_ids, dealer_id, service_type, appointment_at, value, currency, content_type. |
Insurance preset (insurance)
Insurance funnel per the insurance event spec’s Meta mapping: quote requests and leads as Lead, policy views as ViewContent, checkout, payment and policy purchase or renewal as Purchase (policy number as order_id, required value and currency), the quote journey (started, get quote, received, completed) as custom events for custom conversions. Email, phone, name and address are sent only as normalised SHA-256 hashes; content_ids and contents come from items[].
| Signal event | Meta event | Fields the preset sets |
|---|---|---|
page | PageView | global fields only. |
view_item_list | ViewContent | contents, content_ids, value, currency, content_type. |
select_item | ViewContent | contents, content_ids, value, currency, content_type. |
search | Search | search_string. |
view_search_results | Search | search_string. |
view_item | ViewContent | contents, content_ids, value, currency, content_type. |
quote_started | QuoteStarted | value, currency, content_category, quote_id, coverage_level. |
get_quote | GetQuote | value, currency, content_category, quote_id, coverage_level. |
quote_requested | Lead | value, currency, content_category, quote_id, coverage_level. |
quote_received | QuoteReceived | value, currency, content_category, quote_id, coverage_level. |
quote_completed | QuoteCompleted | value, currency, content_category, quote_id, coverage_level. |
generate_lead | Lead | value, currency, content_category, content_name. |
lead_submitted | Lead | value, currency, content_category, content_name. |
contact_requested | Contact | value, currency, content_category, content_name. |
add_to_cart | AddToCart | contents, content_ids, value, currency, content_type. |
begin_checkout | InitiateCheckout | contents, content_ids, value, currency, content_type. |
add_payment_info | AddPaymentInfo | contents, content_ids, value, currency, content_type. |
purchase | Purchase | contents, content_ids, value, currency, order_id, content_category, policy_term_months, content_type. Sent only when properties.currency is set. |
policy_purchased | Purchase | contents, content_ids, value, currency, order_id, content_category, policy_term_months, content_type. Sent only when properties.currency is set. |
policy_renewed | Purchase | contents, content_ids, value, currency, order_id, content_category, policy_term_months, content_type. Sent only when properties.currency is set. |
sign_up | CompleteRegistration | global fields only. |
Example: purchase
A Retail purchase from a known visitor, as Signal sends it to POST https://graph.facebook.com/<api_version>/<pixel_id>/events:
{
"data": [
{
"action_source": "website",
"custom_data": {
"content_ids": [
"STK-7421"
],
"content_type": "product",
"contents": [
{
"id": "STK-7421",
"item_price": 49995,
"quantity": 1
}
],
"currency": "GBP",
"order_id": "ORD-10042",
"value": 49995
},
"event_id": "evt-spec-0001",
"event_name": "Purchase",
"event_source_url": "https://shop.example.co.uk/checkout?gclid=g-123",
"event_time": 1790155800,
"user_data": {
"client_ip_address": "203.0.113.7",
"client_user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Version/18.0 Safari/605.1.15",
"country": "0b407281768f0e833afef47ed464b6571d01ca4d53c12ce5c51d1462f4ad6677",
"ct": "6089854c94ca5454b76be6752c562901a985f64c9a946f62976aeab593b83161",
"em": "86e0b9e56c17cc4d12387e1949b85053fbe73bc3ce5a1188713a9d300cc6133d",
"external_id": "cust-1001",
"fbp": "fb.1.1790150000.111",
"fn": "81f8f6dde88365f3928796ec7aa53f72820b06db8664f5fe76a7eb13e24546a2",
"ln": "799ef92a11af918e3fb741df42934f3b568ed2d93ac1df74f1b8d41a27932a6f",
"ph": "033134b911b137918338415ee3d20a064b24773d36a3b02e8b99fdd3fcd6b4cd",
"st": "82fe032bd9337b5dfb99b28717cf8651daac73f2d319e6d6213544660b961ee7",
"zp": "c69ac90786ee96f8721452ba68d2ebbeaf7938cc797d59bc3303b41af54e02be"
}
}
]
}Event Match Quality (EMQ)
Event Match Quality is Meta’s score (out of 10) indicating how well your server events can be matched to Meta user profiles. A higher EMQ means better ad optimisation and attribution.
How to Improve EMQ
- Send
fbpandfbc: Signal handles this automatically. Thefbpcookie is self-generated andfbcis extracted from thefbclidURL parameter. - Include user data: Pass email and phone via
datafly.identify()when users log in or submit forms. These are hashed before delivery. - Send IP and User-Agent: Signal forwards these automatically from the original browser request.
- Use
external_id: Enabled by default — the hashedanonymous_idis sent asexternal_id.
A typical Signal integration achieves an EMQ of 6-8 with automatic signals alone, and 8-10 when user-provided data (email, phone) is available.
Testing Your Integration
Step 1: Use Test Event Code
- In Meta Events Manager, go to your Pixel > Test Events tab.
- Copy the test event code (e.g.
TEST12345). - Add it to your Signal integration config:
{
"config": {
"pixel_id": "1234567890",
"access_token": "your_access_token",
"test_event_code": "TEST12345"
}
}- Trigger events on your website — they will appear in the Test Events tab within seconds.
Step 2: Verify in Events Manager
- Go to Events Manager > your Pixel > Overview.
- You should see events arriving with the “Server” badge (indicating server-side delivery).
- Click on individual events to inspect the payload and match quality.
Step 3: Check Event Match Quality
- In Events Manager, go to Data Sources > your Pixel > Event Match Quality.
- Review the EMQ score and recommendations.
- If the score is below 6, add more user identity signals via
datafly.identify().
Remove the test_event_code before going to production. Events sent with a test code are not used for ad optimisation or reporting.
Deduplication
If you are running both Signal server-side delivery and the Meta Pixel client-side (during a migration period), you must deduplicate events to prevent double-counting.
Signal includes a unique event_id with every event. To deduplicate:
- Pass the same
event_idto both the client-side Meta Pixel and Datafly.js. - Meta will automatically deduplicate events with matching
event_nameandevent_idreceived within a 48-hour window.
// Generate a shared event ID
const eventId = crypto.randomUUID();
// Send via Meta Pixel (client-side)
fbq('track', 'Purchase', { value: 129.99, currency: 'USD' }, { eventID: eventId });
// Send via Datafly.js (server-side)
datafly.track("Order Completed", {
event_id: eventId,
total: 129.99,
currency: "USD"
});Once you have validated that server-side events are arriving correctly and your EMQ score is healthy, you can remove the client-side Meta Pixel entirely. This eliminates the client-side tag, improves page performance, and removes the deduplication requirement.
Troubleshooting
| Problem | Solution |
|---|---|
| Events not appearing in Events Manager | Check the access token has ads_management permission. Verify the Pixel ID is correct. |
| Low Event Match Quality | Add user identity signals via datafly.identify(). Ensure fbp and fbc cookies are being set. |
OAuthException errors | The access token may have been revoked or the system user lacks permissions. Regenerate the token. |
| Duplicate events | If running both client-side and server-side, ensure matching event_id values for deduplication. |
Invalid parameter errors | Check that required fields (event_name, event_time, action_source) are present. Review the payload in Signal’s event debugger. |
Rate Limits
Meta’s Conversions API supports high throughput:
| Limit | Value |
|---|---|
| Events per API call | 1,000 (batched) |
| Sustained rate | ~1,000 events/second |
Signal batches events automatically when batching is enabled for the integration. For most deployments, the default settings are sufficient.