Meta Conversions API (CAPI) Setup Reference
Complete technical reference for Meta Conversions API: architecture, event types, customer information parameters, implementation methods, deduplication, testing, and Aggregated Event Measurement.
Overview
The Meta Conversions API (CAPI) allows advertisers to send web, app, and offline events directly from their server to Meta. Unlike the browser-based Meta Pixel, CAPI is not affected by ad blockers, cookie restrictions, or browser privacy features such as ITP (Intelligent Tracking Prevention).
Running both the Meta Pixel and CAPI simultaneously is the recommended setup. The Pixel captures in-browser signals (scroll depth, time on page, click interactions), while CAPI provides a reliable server-side data path that survives network interruptions and privacy controls.
Architecture: Browser Pixel vs Server-Side CAPI
User's Browser
┌──────────────────────────────┐
│ Meta Pixel (fbevents.js) │
│ - Fires on page interaction │
│ - Sets _fbp / _fbc cookies │
│ - Sends event_id for dedup │
└──────────┬───────────────────┘
│ HTTPS (browser → Meta)
▼
┌──────────────────────────────┐
│ Meta Servers │
│ - Receives Pixel events │
│ - Receives CAPI events │
│ - Deduplicates via event_id │
│ - Matches users (EMQ) │
│ - Feeds ad optimization │
└──────────────────────────────┘
▲
│ HTTPS (server → Meta)
┌──────────┴───────────────────┐
│ Your Server / GTM SS │
│ - Conversions API endpoint │
│ - POST /v21.0/{pixel_id}/ │
│ events │
│ - Hashes PII before sending │
│ - Sends same event_id │
└──────────────────────────────┘
Why both matter:
- Pixel only: Subject to ad blockers (15-30% signal loss), ITP cookie expiry (7-day cap on Safari), and network failures. Optimization data is incomplete.
- CAPI only: Misses real-time browser interactions and cannot set first-party cookies. Loses behavioral micro-signals the Pixel captures.
- Pixel + CAPI (recommended): Redundant data paths with deduplication. Meta receives events from whichever path succeeds. Typically recovers 10-25% of lost conversions. Required for highest Event Match Quality scores.
Event Types
Standard Events
Standard events are predefined by Meta and enable automatic optimization, reporting, and audience building.
Purchase
Fired when a transaction is completed.
| Parameter | Type | Required | Description |
|---|---|---|---|
value | float | Yes | Total transaction value |
currency | string | Yes | ISO 4217 currency code (e.g., USD, EUR) |
content_ids | array<string> | Recommended | Product IDs from your catalog |
content_type | string | Recommended | product or product_group |
contents | array<object> | Recommended | Array of {id, quantity, item_price} |
content_name | string | Optional | Name of the product or page |
content_category | string | Optional | Product category |
num_items | integer | Optional | Number of items in the order |
order_id | string | Optional | Your internal order ID |
AddToCart
Fired when a user adds an item to the shopping cart.
| Parameter | Type | Required | Description |
|---|---|---|---|
value | float | Recommended | Value of the item added |
currency | string | Recommended | ISO 4217 currency code |
content_ids | array<string> | Recommended | Product IDs |
content_type | string | Recommended | product or product_group |
contents | array<object> | Recommended | Array of {id, quantity, item_price} |
content_name | string | Optional | Product name |
content_category | string | Optional | Product category |
InitiateCheckout
Fired when a user begins the checkout process.
| Parameter | Type | Required | Description |
|---|---|---|---|
value | float | Recommended | Cart value at checkout initiation |
currency | string | Recommended | ISO 4217 currency code |
content_ids | array<string> | Recommended | Product IDs in cart |
content_type | string | Recommended | product or product_group |
contents | array<object> | Recommended | Array of {id, quantity, item_price} |
num_items | integer | Optional | Number of items in cart |
CompleteRegistration
Fired when a user completes a registration form.
| Parameter | Type | Required | Description |
|---|---|---|---|
value | float | Optional | Value assigned to the registration |
currency | string | Optional | ISO 4217 currency code |
content_name | string | Optional | Name of the registration form or page |
status | string | Optional | Registration status (e.g., complete) |
Lead
Fired when a user submits information indicating interest (e.g., form fill, quote request).
| Parameter | Type | Required | Description |
|---|---|---|---|
value | float | Optional | Estimated lead value |
currency | string | Optional | ISO 4217 currency code |
content_name | string | Optional | Name of the lead form or page |
content_category | string | Optional | Lead category |
ViewContent
Fired when a user views a key page (e.g., product page, landing page).
| Parameter | Type | Required | Description |
|---|---|---|---|
value | float | Recommended | Value of the content |
currency | string | Recommended | ISO 4217 currency code |
content_ids | array<string> | Recommended | Product or content IDs |
content_type | string | Recommended | product or product_group |
content_name | string | Optional | Content or product name |
content_category | string | Optional | Content category |
Search
Fired when a user performs a search.
| Parameter | Type | Required | Description |
|---|---|---|---|
search_string | string | Recommended | The search query |
value | float | Optional | Value assigned to the search |
currency | string | Optional | ISO 4217 currency code |
content_ids | array<string> | Optional | IDs of search results shown |
content_category | string | Optional | Category of search results |
AddPaymentInfo
Fired when a user adds payment information during checkout.
| Parameter | Type | Required | Description |
|---|---|---|---|
value | float | Optional | Cart value |
currency | string | Optional | ISO 4217 currency code |
content_ids | array<string> | Optional | Product IDs |
content_type | string | Optional | product or product_group |
content_category | string | Optional | Payment method category |
AddToWishlist
Fired when a user adds an item to a wishlist.
| Parameter | Type | Required | Description |
|---|---|---|---|
value | float | Optional | Value of the item |
currency | string | Optional | ISO 4217 currency code |
content_ids | array<string> | Optional | Product IDs |
content_name | string | Optional | Product name |
content_category | string | Optional | Product category |
Contact
Fired when a user initiates contact (phone call, SMS, email, chat).
| Parameter | Type | Required | Description |
|---|---|---|---|
| No required or recommended parameters. Send customer information parameters for matching. |
CustomizeProduct
Fired when a user customizes a product (e.g., selects color, size, configuration).
| Parameter | Type | Required | Description |
|---|---|---|---|
content_ids | array<string> | Optional | Product IDs |
content_type | string | Optional | product or product_group |
content_name | string | Optional | Product name |
Donate
Fired when a user makes a donation.
| Parameter | Type | Required | Description |
|---|---|---|---|
value | float | Recommended | Donation amount |
currency | string | Recommended | ISO 4217 currency code |
FindLocation
Fired when a user searches for a physical location (store locator, branch finder).
| Parameter | Type | Required | Description |
|---|---|---|---|
content_name | string | Optional | Location name or search query |
content_category | string | Optional | Location type |
Schedule
Fired when a user books an appointment or schedules a visit.
| Parameter | Type | Required | Description |
|---|---|---|---|
content_name | string | Optional | Appointment type |
content_category | string | Optional | Service category |
StartTrial
Fired when a user starts a free trial.
| Parameter | Type | Required | Description |
|---|---|---|---|
value | float | Recommended | Predicted trial value |
currency | string | Recommended | ISO 4217 currency code |
content_name | string | Optional | Trial plan name |
SubmitApplication
Fired when a user submits an application (loan, job, program).
| Parameter | Type | Required | Description |
|---|---|---|---|
content_name | string | Optional | Application type |
content_category | string | Optional | Application category |
Subscribe
Fired when a user starts a paid subscription.
| Parameter | Type | Required | Description |
|---|---|---|---|
value | float | Yes | Subscription value |
currency | string | Yes | ISO 4217 currency code |
content_name | string | Optional | Subscription plan name |
Custom Events
Custom events are for actions not covered by standard events. They can be used for audience building and custom conversions but cannot power standard optimization objectives.
Naming rules:
- Maximum 40 characters
- Use lowercase letters, numbers, and underscores only
- Must not start with a number
- Must not use reserved names (any standard event name,
fb_,_fb, or Meta internal prefixes) - Examples:
qualified_lead,demo_booked,pricing_page_scroll_50
When to use custom events:
- When no standard event maps to your conversion action
- For micro-conversions used in audience building (e.g.,
video_75_percent,scroll_depth_90) - For offline or CRM-based events (e.g.,
sql_qualified,deal_closed)
Customer Information Parameters
Customer information parameters (also called user data) are sent alongside events to enable Meta to match events to user profiles. Higher match quality directly improves ad optimization and reporting accuracy.
Available Parameters
| Parameter | Key | Description | Format Before Hashing |
|---|---|---|---|
em | User email address | Lowercase, trim whitespace | |
| Phone | ph | Phone number with country code | Digits only, include country code (e.g., 14155551234) |
| First Name | fn | User first name | Lowercase, trim, remove punctuation |
| Last Name | ln | User last name | Lowercase, trim, remove punctuation |
| City | ct | User city | Lowercase, trim, no punctuation, no spaces |
| State | st | State or province | 2-letter ANSI abbreviation, lowercase (e.g., ca) |
| Zip Code | zp | Postal/zip code | Trim. US: 5-digit only (no +4). UK: area + district format |
| Country | country | Country code | 2-letter ISO 3166-1 alpha-2, lowercase (e.g., us) |
| Date of Birth | db | Date of birth | YYYYMMDD format (e.g., 19910526) |
| Gender | ge | Gender | Single letter: m or f |
| External ID | external_id | Your internal user ID | Any string. Hash recommended but not required |
Hashing Requirements
All customer information parameters except external_id must be hashed using SHA-256 before sending to Meta via CAPI. The Meta Pixel hashes automatically; CAPI does not.
Hashing procedure:
- Normalize the value (lowercase, trim whitespace, remove special characters per the format rules above)
- Hash using SHA-256
- Encode as a lowercase hexadecimal string (64 characters)
// Node.js example
const crypto = require('crypto');
function hashForMeta(value) {
if (!value) return null;
const normalized = value.toString().trim().toLowerCase();
return crypto.createHash('sha256').update(normalized).digest('hex');
}
// Examples
hashForMeta('John'); // fn
hashForMeta('john@example.com'); // em
hashForMeta('14155551234'); // ph (digits only, with country code)
# Python example
import hashlib
def hash_for_meta(value):
if not value:
return None
normalized = str(value).strip().lower()
return hashlib.sha256(normalized.encode('utf-8')).hexdigest()
Common hashing mistakes:
- Hashing before normalizing (uppercase letters in the hash input)
- Double-hashing (hashing an already-hashed value)
- Including whitespace in the hash input
- Not including country code in phone numbers
- Using MD5 instead of SHA-256
Event Match Quality (EMQ)
Event Match Quality is a score from 1 to 10 that Meta assigns to each event, indicating how well Meta could match the event to a user profile. Higher EMQ means better ad optimization.
How EMQ is calculated:
- Based on the number and quality of customer information parameters sent
- Each additional parameter increases match probability
em(email) has the highest individual impact- Combinations matter:
em+ph+fn+lnis significantly better thanemalone
Target scores:
| EMQ Score | Quality | Impact |
|---|---|---|
| 1-3 | Poor | Most events unmatched; optimization severely limited |
| 4-5 | Fair | Partial matching; optimization suboptimal |
| 6-7 | Good | Strong matching; optimization works well |
| 8-10 | Excellent | Near-complete matching; full optimization potential |
How to improve EMQ:
- Send
em(email) with every event where available -- single biggest impact - Add
ph(phone) as a second identifier - Include
fn+lnto disambiguate common emails - Pass
external_idto enable cross-device matching - Forward
fbpandfbccookies from the browser to your server - Ensure correct normalization and hashing -- malformed hashes count as missing data
Implementation Methods
1. Direct API Integration
Send events directly from your application server to the Meta Conversions API endpoint.
Endpoint: POST https://graph.facebook.com/v21.0/{pixel_id}/events
Required headers:
Content-Type: application/json
Required query parameters:
access_token-- System User token withads_managementormanage_pagespermission
Node.js Implementation
const crypto = require('crypto');
const https = require('https');
const PIXEL_ID = 'YOUR_PIXEL_ID';
const ACCESS_TOKEN = 'YOUR_ACCESS_TOKEN';
const API_VERSION = 'v21.0';
function hash(value) {
if (!value) return undefined;
return crypto.createHash('sha256')
.update(value.toString().trim().toLowerCase())
.digest('hex');
}
async function sendEvent(eventName, eventData, userData, eventId = null) {
const payload = {
data: [{
event_name: eventName,
event_time: Math.floor(Date.now() / 1000),
event_id: eventId || crypto.randomUUID(),
event_source_url: eventData.source_url,
action_source: 'website',
user_data: {
em: hash(userData.email),
ph: hash(userData.phone),
fn: hash(userData.firstName),
ln: hash(userData.lastName),
ct: hash(userData.city),
st: hash(userData.state),
zp: hash(userData.zipCode),
country: hash(userData.country),
external_id: hash(userData.externalId),
client_ip_address: userData.ipAddress,
client_user_agent: userData.userAgent,
fbp: userData.fbp, // _fbp cookie value (not hashed)
fbc: userData.fbc, // _fbc cookie value (not hashed)
},
custom_data: eventData.customData || {},
}],
};
const url = `https://graph.facebook.com/${API_VERSION}/${PIXEL_ID}/events?access_token=${ACCESS_TOKEN}`;
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const result = await response.json();
if (!response.ok) {
throw new Error(`CAPI error: ${JSON.stringify(result)}`);
}
return result;
}
// Example: Send a Purchase event
await sendEvent('Purchase', {
source_url: 'https://example.com/checkout/thank-you',
customData: {
value: 99.99,
currency: 'USD',
content_ids: ['SKU-123', 'SKU-456'],
content_type: 'product',
order_id: 'ORDER-789',
num_items: 2,
},
}, {
email: 'john@example.com',
phone: '14155551234',
firstName: 'John',
lastName: 'Doe',
ipAddress: '203.0.113.50',
userAgent: 'Mozilla/5.0 ...',
fbp: 'fb.1.1612345678901.1234567890',
fbc: 'fb.1.1612345678901.AbCdEfGhIjKl',
externalId: 'user-12345',
}, 'evt_purchase_abc123');
Python Implementation
import hashlib
import time
import uuid
import requests
PIXEL_ID = 'YOUR_PIXEL_ID'
ACCESS_TOKEN = 'YOUR_ACCESS_TOKEN'
API_VERSION = 'v21.0'
def hash_value(value):
if not value:
return None
normalized = str(value).strip().lower()
return hashlib.sha256(normalized.encode('utf-8')).hexdigest()
def send_event(event_name, event_data, user_data, event_id=None):
url = f'https://graph.facebook.com/{API_VERSION}/{PIXEL_ID}/events'
payload = {
'data': [{
'event_name': event_name,
'event_time': int(time.time()),
'event_id': event_id or str(uuid.uuid4()),
'event_source_url': event_data.get('source_url'),
'action_source': 'website',
'user_data': {
'em': hash_value(user_data.get('email')),
'ph': hash_value(user_data.get('phone')),
'fn': hash_value(user_data.get('first_name')),
'ln': hash_value(user_data.get('last_name')),
'ct': hash_value(user_data.get('city')),
'st': hash_value(user_data.get('state')),
'zp': hash_value(user_data.get('zip_code')),
'country': hash_value(user_data.get('country')),
'external_id': hash_value(user_data.get('external_id')),
'client_ip_address': user_data.get('ip_address'),
'client_user_agent': user_data.get('user_agent'),
'fbp': user_data.get('fbp'),
'fbc': user_data.get('fbc'),
},
'custom_data': event_data.get('custom_data', {}),
}],
'access_token': ACCESS_TOKEN,
}
# Remove None values from user_data
payload['data'][0]['user_data'] = {
k: v for k, v in payload['data'][0]['user_data'].items()
if v is not None
}
response = requests.post(url, json=payload)
response.raise_for_status()
return response.json()
# Example: Send a Purchase event
send_event('Purchase', {
'source_url': 'https://example.com/checkout/thank-you',
'custom_data': {
'value': 99.99,
'currency': 'USD',
'content_ids': ['SKU-123', 'SKU-456'],
'content_type': 'product',
'order_id': 'ORDER-789',
'num_items': 2,
},
}, {
'email': 'john@example.com',
'phone': '14155551234',
'first_name': 'John',
'last_name': 'Doe',
'ip_address': '203.0.113.50',
'user_agent': 'Mozilla/5.0 ...',
'fbp': 'fb.1.1612345678901.1234567890',
'fbc': 'fb.1.1612345678901.AbCdEfGhIjKl',
'external_id': 'user-12345',
}, event_id='evt_purchase_abc123')
PHP Implementation
<?php
$pixelId = 'YOUR_PIXEL_ID';
$accessToken = 'YOUR_ACCESS_TOKEN';
$apiVersion = 'v21.0';
function hashForMeta(?string $value): ?string {
if (empty($value)) return null;
$normalized = strtolower(trim($value));
return hash('sha256', $normalized);
}
function sendEvent(string $eventName, array $eventData, array $userData, ?string $eventId = null): array {
global $pixelId, $accessToken, $apiVersion;
$payload = [
'data' => [json_encode([[
'event_name' => $eventName,
'event_time' => time(),
'event_id' => $eventId ?? bin2hex(random_bytes(16)),
'event_source_url' => $eventData['source_url'] ?? null,
'action_source' => 'website',
'user_data' => array_filter([
'em' => hashForMeta($userData['email'] ?? null),
'ph' => hashForMeta($userData['phone'] ?? null),
'fn' => hashForMeta($userData['first_name'] ?? null),
'ln' => hashForMeta($userData['last_name'] ?? null),
'ct' => hashForMeta($userData['city'] ?? null),
'st' => hashForMeta($userData['state'] ?? null),
'zp' => hashForMeta($userData['zip_code'] ?? null),
'country' => hashForMeta($userData['country'] ?? null),
'external_id' => hashForMeta($userData['external_id'] ?? null),
'client_ip_address' => $userData['ip_address'] ?? null,
'client_user_agent' => $userData['user_agent'] ?? null,
'fbp' => $userData['fbp'] ?? null,
'fbc' => $userData['fbc'] ?? null,
]),
'custom_data' => $eventData['custom_data'] ?? new \stdClass(),
]])],
'access_token' => $accessToken,
];
$url = "https://graph.facebook.com/{$apiVersion}/{$pixelId}/events";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($payload));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
2. GTM Server-Side Container
Google Tag Manager server-side containers can forward events to Meta CAPI without custom server code.
Setup steps:
- Deploy a GTM server-side container (Google Cloud Run, AWS, or other hosting)
- Create a Meta Conversions API tag in your server container
- Configure the tag:
- API Access Token: Your system user token
- Pixel ID: Your Meta Pixel ID
- Action Source:
website - Event Name: Map from the incoming event (e.g., GA4
purchase→ MetaPurchase)
- Set up a trigger to fire on the relevant events forwarded from your web container
- Map user data parameters (email, phone, etc.) from the incoming event data
- Set the
event_idto match the browser Pixel'seventIDfor deduplication
GTM Server-Side Tag Configuration (key fields):
Tag Type: Meta Conversions API
Pixel ID: {{Meta Pixel ID}}
API Access Token: {{Meta CAPI Token}}
Event Name: {{Event Name}}
Action Source: website
User Data:
Email: {{User Email - Hashed}}
Phone: {{User Phone - Hashed}}
First Name: {{User First Name - Hashed}}
Last Name: {{User Last Name - Hashed}}
IP Address: {{Client IP}} (auto-populated in SS GTM)
User Agent: {{Client User Agent}} (auto-populated)
FBP: {{FBP Cookie}}
FBC: {{FBC Cookie}}
Event Parameters:
Event ID: {{Event ID}} (must match browser Pixel eventID)
Event Source URL: {{Page URL}}
Value: {{Event Value}}
Currency: {{Event Currency}}
Content IDs: {{Content IDs}}
Content Type: {{Content Type}}
3. Partner Integrations
Many e-commerce platforms offer built-in CAPI integrations:
| Platform | Integration Method | Notes |
|---|---|---|
| Shopify | Native integration in Meta channel app | Automatic dedup; sends Purchase, AddToCart, ViewContent, InitiateCheckout, Search, AddPaymentInfo |
| WooCommerce | Facebook for WooCommerce plugin | Requires configuration; sends standard e-commerce events |
| BigCommerce | Native Meta integration | Automatic CAPI via app |
| Magento/Adobe Commerce | Meta Business Extension | Server-side events via extension |
| WordPress | PixelYourSite or official Meta plugin | Varies by plugin quality |
Considerations for partner integrations:
- Limited control over which parameters are sent
- May not support all standard events
- EMQ depends on what customer data the platform passes
- Always verify in Events Manager that events are arriving with expected parameters
4. Measurement Protocol Comparison
The Meta Conversions API is conceptually similar to Google Analytics 4 Measurement Protocol, but with key differences:
| Feature | Meta CAPI | GA4 Measurement Protocol |
|---|---|---|
| Endpoint | graph.facebook.com/v21.0/{pixel_id}/events | www.google-analytics.com/mp/collect |
| Auth | System User access token | API secret + measurement ID |
| PII handling | SHA-256 hashing required | No PII accepted |
| Dedup | event_id + event_name matching | Not applicable (no browser counterpart) |
| Batching | Up to 1,000 events per request | Up to 25 events per request |
| Latency | Real-time processing | Up to 72 hours for reporting |
| Primary use | Ad optimization + attribution | Analytics reporting |
Event Deduplication
When running both the Meta Pixel and CAPI, the same conversion can be reported twice. Meta deduplicates events using the combination of event_id and event_name.
How Deduplication Works
- Browser Pixel fires an event with
eventID: "evt_abc123"andevent_name: "Purchase" - Your server sends the same event via CAPI with
event_id: "evt_abc123"andevent_name: "Purchase" - Meta sees both events, matches on
event_id+event_name, and counts only one conversion - If only one path succeeds (e.g., ad blocker stops the Pixel), the other path still delivers the event
Dedup Strategy
// 1. Generate event_id in the browser BEFORE firing the Pixel event
const eventId = crypto.randomUUID(); // e.g., "evt_abc123"
// 2. Fire the Pixel event with this eventID
fbq('track', 'Purchase', {
value: 99.99,
currency: 'USD',
content_ids: ['SKU-123'],
}, { eventID: eventId });
// 3. Send the same eventId to your server (via form data, AJAX, or data layer)
fetch('/api/track', {
method: 'POST',
body: JSON.stringify({
event_name: 'Purchase',
event_id: eventId,
value: 99.99,
currency: 'USD',
content_ids: ['SKU-123'],
}),
});
// 4. Server forwards to CAPI with the same event_id
// (see Node.js / Python examples above)
fbp and fbc Cookie Handling
_fbp (Facebook Browser ID):
- Set automatically by the Meta Pixel on first page load
- Format:
fb.1.{timestamp}.{random_number}(e.g.,fb.1.1612345678901.1234567890) - Persists for 90 days as a first-party cookie
- Read it server-side from the cookie header and pass as
fbpin CAPI user_data - Do not hash -- send the raw cookie value
_fbc (Facebook Click ID):
- Set when a user clicks a Meta ad (contains the
fbclidURL parameter) - Format:
fb.1.{timestamp}.{fbclid_value}(e.g.,fb.1.1612345678901.AbCdEfGhIjKlMnOp) - If not present as a cookie, you can construct it from the
fbclidURL parameter:fb.1.{current_timestamp_ms}.{fbclid} - Do not hash -- send the raw value
// Server-side: extract fbp and fbc from cookies
function extractMetaCookies(cookieHeader) {
const cookies = Object.fromEntries(
cookieHeader.split(';').map(c => c.trim().split('='))
);
return {
fbp: cookies['_fbp'] || null,
fbc: cookies['_fbc'] || null,
};
}
// If _fbc is missing but fbclid is in the URL
function constructFbc(fbclid) {
if (!fbclid) return null;
return `fb.1.${Date.now()}.${fbclid}`;
}
Common Dedup Failures
| Problem | Symptom | Fix |
|---|---|---|
Different event_id on Pixel vs CAPI | Double-counted conversions | Generate event_id in browser, pass to server |
event_id missing from Pixel | No dedup possible | Add { eventID: id } as 4th argument to fbq('track', ...) |
event_id missing from CAPI | No dedup possible | Include event_id in CAPI payload |
Different event_name casing | Events not matched | Use exact standard event names (e.g., Purchase, not purchase) |
| CAPI event sent hours later | Events outside dedup window (48h) | Send CAPI events within minutes, not in delayed batches |
| Pixel fires multiple times | Multiple Pixel events with different IDs | Ensure Pixel fires only once per conversion action |
Testing and Validation
Test Events Tool
Meta provides a Test Events tool in Events Manager to validate your CAPI implementation before going live.
How to use:
- Open Events Manager > Select your Pixel > Test Events tab
- Copy the Test Event Code (e.g.,
TEST12345) - Add
test_event_codeto your CAPI payload:
const payload = {
data: [{ /* your event data */ }],
test_event_code: 'TEST12345', // Add this field
};
- Send events -- they appear in the Test Events tab in real time
- Remove
test_event_codebefore going to production -- test events are not processed for ad delivery
Payload Inspection
Use the Events Manager Overview tab to inspect received events:
- Events Received: Total events arriving via Pixel and CAPI
- Events Matched: Events successfully matched to a Meta user
- Event Match Quality: Per-event EMQ score breakdown
- Dedup Rate: Percentage of events deduplicated (indicates both paths are working)
- Top Parameters: Which customer info parameters are being sent most frequently
Event Match Quality Dashboard
Navigate to Events Manager > Data Sources > Your Pixel > Event Match Quality to see:
- Per-event EMQ scores
- Which parameters are most frequently sent
- Recommendations for improving match quality
- Trend over time
Common Error Codes and Fixes
| Error Code | Message | Fix |
|---|---|---|
190 | Invalid OAuth access token | Regenerate your system user token; check it has not expired |
100 | Invalid parameter | Check required fields: event_name, event_time, action_source, and at least one user_data field |
2804003 | Event timestamp too old | event_time must be within the last 7 days (ideally within minutes) |
2804004 | Invalid event_time | event_time must be a Unix timestamp in seconds (not milliseconds) |
2804001 | Missing user_data | Include at least one customer information parameter |
368 | Temporarily blocked | Rate limiting; implement exponential backoff |
2804002 | Invalid action_source | Must be one of: website, app, phone_call, chat, email, in_store, other |
803 | Permission denied | System user needs ads_management permission for the ad account |
Aggregated Event Measurement (AEM)
What It Is
Aggregated Event Measurement (AEM) is Meta's protocol for measuring web and app events from iOS 14.5+ users who have opted out of tracking via Apple's App Tracking Transparency (ATT) prompt. It was introduced in 2021 to maintain some conversion measurement capability under Apple's privacy restrictions.
How AEM Works
- Limited data: Meta receives delayed, aggregated conversion data for opted-out iOS users (not real-time, not user-level)
- 8-event limit: You can configure up to 8 conversion events per domain, ranked by priority
- Statistical modeling: Meta uses statistical methods to estimate conversions from opted-out users
- 72-hour delay: Conversion data from opted-out users may take up to 3 days to appear in reporting
- 1-day attribution: AEM restricts attribution to a 1-day click window for opted-out users (vs. the standard 7-day click / 1-day view default)
8-Event Priority Ranking
You configure up to 8 events per domain in Events Manager, ranked from highest to lowest priority. When a user performs multiple conversion actions in a single session, only the highest-priority event is reported.
Recommended priority ranking for e-commerce:
| Priority | Event | Rationale |
|---|---|---|
| 1 (highest) | Purchase | Highest-value conversion |
| 2 | Subscribe | Recurring revenue signal |
| 3 | StartTrial | Trial-to-paid pipeline |
| 4 | InitiateCheckout | Strong purchase intent |
| 5 | AddPaymentInfo | Checkout progression |
| 6 | AddToCart | Mid-funnel engagement |
| 7 | CompleteRegistration | Account creation |
| 8 (lowest) | ViewContent | Top-of-funnel awareness |
Recommended priority ranking for lead generation:
| Priority | Event | Rationale |
|---|---|---|
| 1 (highest) | Purchase | Closed deal |
| 2 | Lead | Form submission |
| 3 | SubmitApplication | Application submitted |
| 4 | Schedule | Demo/appointment booked |
| 5 | CompleteRegistration | Account created |
| 6 | Contact | Initiated contact |
| 7 | ViewContent | Key page viewed |
| 8 (lowest) | Search | Site search performed |
Domain Verification
AEM requires domain verification to configure event priority:
- Go to Business Settings > Brand Safety > Domains
- Add your domain and verify via one of:
- DNS TXT record (recommended): Add a TXT record to your domain's DNS
- HTML file upload: Upload a verification file to your web root
- Meta tag: Add a meta tag to your homepage
<head>
- Once verified, configure your 8 events in Events Manager > Aggregated Event Measurement
Impact on Reporting
| Metric | Pre-iOS 14.5 | Post-iOS 14.5 (AEM) |
|---|---|---|
| Attribution window | 28-day click, 7-day view | 7-day click, 1-day view (default) |
| Reporting delay | Real-time | Up to 72 hours for opted-out users |
| Breakdown dimensions | Full (age, gender, placement, etc.) | Limited (some breakdowns unavailable) |
| Conversion count | Exact | Modeled/estimated for opted-out users |
| Optimization events | Unlimited | 8 per domain |
Mitigations:
- Use CAPI to maximize matched events (higher EMQ partially offsets ATT opt-outs)
- Prioritize the 8 events that matter most for optimization
- Expect 15-30% underreporting on iOS conversions; use server-side data as source of truth
- Consider broad targeting strategies that rely less on individual user tracking
Browser Pixel + CAPI Dedup Pattern (Complete Example)
This example shows the full end-to-end pattern for a Purchase event with proper deduplication.
Browser Side
<!-- Meta Pixel base code (in <head>) -->
<script>
!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;
n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,
document,'script','https://connect.facebook.net/en_US/fbevents.js');
fbq('init', 'YOUR_PIXEL_ID');
fbq('track', 'PageView');
</script>
<!-- On the purchase confirmation page -->
<script>
(function() {
// Generate a unique event_id for deduplication
var eventId = 'purchase_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
// Fire the Pixel event with eventID
fbq('track', 'Purchase', {
value: 149.99,
currency: 'USD',
content_ids: ['SKU-001', 'SKU-002'],
content_type: 'product',
num_items: 2,
}, { eventID: eventId });
// Send the same event data + event_id to your server for CAPI
navigator.sendBeacon('/api/meta-capi', JSON.stringify({
event_name: 'Purchase',
event_id: eventId,
source_url: window.location.href,
custom_data: {
value: 149.99,
currency: 'USD',
content_ids: ['SKU-001', 'SKU-002'],
content_type: 'product',
num_items: 2,
},
}));
})();
</script>
Server Side
// POST /api/meta-capi
app.post('/api/meta-capi', async (req, res) => {
const { event_name, event_id, source_url, custom_data } = req.body;
// Extract user data from your session/database
const user = await getAuthenticatedUser(req);
// Extract Meta cookies
const fbp = req.cookies['_fbp'];
const fbc = req.cookies['_fbc'] || constructFbc(req.query.fbclid);
await sendEvent(event_name, {
source_url,
customData: custom_data,
}, {
email: user?.email,
phone: user?.phone,
firstName: user?.firstName,
lastName: user?.lastName,
ipAddress: req.ip,
userAgent: req.headers['user-agent'],
fbp,
fbc,
externalId: user?.id,
}, event_id); // Same event_id as the Pixel
res.status(200).json({ ok: true });
});
Next Steps
- Google Ads API Requirements -- Set up cross-channel tracking
- BigQuery Service Account Setup -- Store conversion data in BigQuery
- AI Report Generation -- Automate conversion analysis with Cogny
Claude Code Skill
This CAPI reference is also available as a free Claude Code skill -- use it directly in your terminal:
# Install
curl -sSL https://raw.githubusercontent.com/cognyai/claude-code-marketing-skills/main/install.sh | bash
# Use
/meta-capi # Full CAPI overview
/meta-capi deduplication # Event dedup strategy
/meta-capi purchase event # Purchase event parameters
/meta-capi emq # Improve Event Match Quality
Resources
- Meta Conversions API Documentation: developers.facebook.com/docs/marketing-api/conversions-api
- Conversions API Setup Guide: developers.facebook.com/docs/marketing-api/conversions-api/get-started
- Event Match Quality Best Practices: developers.facebook.com/docs/marketing-api/conversions-api/best-practices
- Standard Events Reference: developers.facebook.com/docs/meta-pixel/reference
- Aggregated Event Measurement: developers.facebook.com/docs/marketing-apis/data-processing-options
- Claude Code Marketing Skills: github.com/cognyai/claude-code-marketing-skills