Meta Attribution Setup Guide
Complete dual-tracking stack for Meta: Pixel + Conversions API, Advanced Matching, EMQ optimization, AEM, Consent Mode, and 2026 deduplication rules.
Why Dual Tracking (Pixel + Conversions API)?
Meta's signal stack has two layers — and in 2026 you need both. Browser-only tracking has lost roughly 20–30% of events to ad blockers, ITP/ETP, iOS App Tracking Transparency (ATT), and the gradual death of third-party cookies. The Conversions API (CAPI) is how you recover that signal.
| Layer | Mechanism | What it captures | What it misses |
|---|---|---|---|
| Browser-side | Meta Pixel (fbevents.js) | Real-time UI events, fbp/fbc cookies, full page context, Advanced Matching auto-collected fields | Everything blocked by ad blockers, Safari ITP, iOS ATT, Brave, Firefox ETP, server-only events (refunds, subscription renewals, offline conversions) |
| Server-side | Conversions API (HTTPS POST) | Same events, plus authoritative server-truth signals (Stripe webhook purchases, CRM lead status changes, refunds, churn) | The browser context unless you forward fbp/fbc/client_user_agent/client_ip_address from the request |
CAPI advantages worth calling out:
- Lower latency for high-value signals. A Stripe webhook fires within ~1s of a charge succeeding; the browser
Purchasepixel may never fire if the user closes the tab on the thank-you page. - Retries. A failed
fetchfromfbevents.jsis gone. A failed CAPI POST can be queued and retried server-side. - Server-only events. Subscription renewals, refunds, lead-qualified, churn — these never happen in a browser. CAPI is the only path.
- Better EMQ. When you send
fbp+fbc+ hashedemail+ hashedphone+client_ip_address+client_user_agentfrom the server, your Event Match Quality score climbs into the 8–10 range, which directly improves Advantage+ optimization.
Rule of thumb: Pixel for the algorithm's real-time loop, CAPI for the source of truth. Same
event_idon both, dedup window does the rest.
Step 1 — Credentials & Access
Pixel ID (a.k.a. Dataset ID)
Meta has been gradually renaming Pixel to Dataset in Events Manager because a single dataset can now ingest web, app, offline, and CRM events. The ID is the same — just sometimes labeled "Dataset ID" in newer UI surfaces.
- Go to business.facebook.com/events_manager2.
- Select your data source (Pixel/Dataset).
- Settings → copy the Dataset ID (15–16 digit number).
CAPI Access Token
Two ways to mint a token. Pick System User for production.
| Method | When to use | Lifetime |
|---|---|---|
| System User token (recommended) | Production. Tied to a Business Manager system user, not a human admin. Survives employee turnover. | Configurable; "Never expire" supported |
| User access token via the Conversions API → Generate Access Token button | Quick prototyping, local testing only | 60 days |
To create a System User token:
- business.facebook.com/settings/system-users → Add → name it (e.g.
orbit-capi). - Assign assets: your Ad Account + Pixel/Dataset, with Manage Pixel permission.
- Generate New Token → select your app → scope:
ads_management,business_management→ set expiration to Never. - Store in your secret manager. Never commit to source.
Domain Verification
Required before you can configure most CAPI features at scale and to claim ad permissions on shared domains. Three methods, ranked:
| Method | Where | Pros | Cons |
|---|---|---|---|
| DNS TXT record | Add facebook-domain-verification=<code> TXT record at root | Most robust — survives theme changes, deploys, CDN moves | Requires DNS access; propagation 5min–48h |
| Meta tag | <meta name="facebook-domain-verification" content="..." /> in <head> of root domain | Fast, works in most CMS | Lost if theme is replaced; must be on every page of root |
| HTML file upload | Upload provided .html to web root | No DNS needed | Brittle; many CDNs strip unknown files |
Prereq notes: enter the apex domain only (example.com, not www.example.com or https://example.com). Verification status takes effect in Business Settings → Brand Safety → Domains.
Step 2 — Standard Events Reference
Meta's algorithm optimizes against named standard events. Custom events work but get less optimization weight. Spelling matters.
| Funnel Stage | Event Name | Required Params | Recommended Params | When to fire |
|---|---|---|---|---|
| Page load | PageView | — | — | Auto-fired by base pixel; mirror server-side only if running pixel-less |
| Product detail page | ViewContent | — | content_ids, content_type (product), value, currency | User lands on a PDP |
| Add to cart | AddToCart | contents (req. for Advantage+ catalog) | value, currency, content_ids | User clicks add-to-cart |
| Checkout start | InitiateCheckout | — | value, currency, num_items | User enters checkout flow |
| Payment info | AddPaymentInfo | — | value, currency | Card/billing info captured |
| Purchase | Purchase | currency, value; contents or content_ids for Advantage+ | order_id, num_items | Order confirmed (fire from server on Stripe payment_intent.succeeded) |
| Account creation | CompleteRegistration | — | value, currency, status | Signup form submitted |
| Lead capture | Lead | — | value, currency, lead_event_source, content_category | Lead form submitted |
| Subscription start | Subscribe | — | value, currency, predicted_ltv | Paid subscription begins |
| Contact | Contact | — | — | User initiates contact (call, email click) |
| Search | Search | contents or content_ids for Advantage+ | search_string, value, currency | Site search executed |
| Find location | FindLocation | — | — | Store locator used |
| Customize product | CustomizeProduct | — | — | Configurator interaction |
| Donate | Donate | — | value, currency | Donation completed |
| Schedule | Schedule | — | — | Appointment booked |
| Start trial | StartTrial | — | value, currency, predicted_ltv | Free trial begins |
| Submit application | SubmitApplication | — | — | Application form submitted |
purchase ≠ Purchase. A misspelled event is accepted as a custom event and gets minimal optimization weight. The browser pixel and CAPI both must use the exact spelling above.contents is the structured form: [{ id: "SKU-123", quantity: 1, item_price: 29.99 }].Step 3 — Customer Information Parameters (CAPI user_data)
This is where the EMQ score lives or dies. The richer your user_data block, the higher your match rate. Every PII field below must be SHA-256 hashed before sending. Hash failures = silent match failures = wasted spend.
| Param | Field | Hash? | Normalization (before hash) | Example raw → hashed |
|---|---|---|---|---|
em | Yes | Trim, lowercase | Jane@Example.com → jane@example.com → 5e88... | |
ph | Phone | Yes | Strip all non-digits, include country code (E.164 without +) | +1 (212) 555-1234 → 12125551234 → 4d2c... |
fn | First name | Yes | Trim, lowercase, strip punctuation | Jane → jane |
ln | Last name | Yes | Trim, lowercase, strip punctuation | O'Neill → oneill |
ct | City | Yes | Trim, lowercase, no spaces or punctuation | San Francisco → sanfrancisco |
st | State | Yes | Lowercase 2-letter ANSI code (US) or full lowercase elsewhere | CA → ca |
zp | Postal code | Yes | First 5 digits only (US ZIP), trim/lowercase | 94103-1234 → 94103 |
country | Country | Yes | Lowercase ISO 3166-1 alpha-2 | US → us |
db | Date of birth | Yes | YYYYMMDD | 1985-04-15 → 19850415 → hash |
ge | Gender | Yes | Single lowercase char: m or f | Female → f |
external_id | Your user/customer ID | Yes (recommended) | Trim, lowercase | cust_4f9a → hash |
client_ip_address | User IP | No — raw | — | 203.0.113.42 |
client_user_agent | User agent | No — raw | — | Mozilla/5.0 ... |
fbc | Click ID cookie | No — raw | Format: fb.<sub>.<ts>.<fbclid> | fb.1.1714435200000.IwAR0Xy... |
fbp | Browser ID cookie | No — raw | Format: fb.<sub>.<ts>.<rand> | fb.1.1714435200000.987654321 |
subscription_id | Recurring subscription ID | No | — | sub_1NxYz... |
lead_id | Meta Lead Ad lead ID | No | — | 123456789012345 |
fb_login_id | Facebook user ID (logged-in users) | No | — | 100012345678901 |
anon_id | Mobile app installation ID | No | — | App-generated UUID |
madid | Mobile advertising ID (IDFA/GAID) | No | Lowercase | IDFA-XXXX-XXXX |
Common hashing mistakes that nuke your EMQ:
- Hashing the raw email without lowercasing first →
Jane@example.comandjane@example.comproduce different hashes; Meta will fail to match against its own internally-lowercased index. - Phone numbers with
+, spaces, or()not stripped before hashing. - Sending
countryasUSAinstead ofus. - Double-hashing — if your platform already hashes (Shopify, GTM templates), don't hash again.
_fbp and _fbc cookie format
Both are first-party cookies set by fbevents.js. CAPI expects them verbatim from the cookie value:
_fbp = fb.<subdomainIndex>.<creationTimeMs>.<random10digit>
e.g. fb.1.1714435200000.987654321
_fbc = fb.<subdomainIndex>.<creationTimeMs>.<fbclid>
e.g. fb.1.1714435200000.IwAR0Xy7QzExample...
subdomainIndex: 0 = .com, 1 = example.com, 2 = www.example.com. The pixel script handles this. _fbc is created when a user lands with ?fbclid=.... If you bypass the pixel, you must construct _fbc yourself from the URL fbclid.
// Manually construct fbc when pixel is unavailable
const params = new URLSearchParams(window.location.search);
const fbclid = params.get('fbclid');
if (fbclid) {
const fbc = `fb.1.${Date.now()}.${fbclid}`;
document.cookie = `_fbc=${fbc}; path=/; max-age=${60*60*24*90}; SameSite=Lax; Secure`;
}
Cookies persist 90 days by default. Send both in every CAPI event for users who have them.
Step 4 — Advanced Matching (Browser-Side)
Advanced Matching is how the Pixel gets PII into Meta's matching graph from the browser. Two flavors:
Automatic Advanced Matching (AAM)
Toggle in Events Manager → Settings → Automatic Advanced Matching. Meta's pixel scrapes form fields (email, phone, name, address) using heuristics, hashes in-browser with SHA-256, and ships with each event. Zero code, but coverage is variable depending on your form markup.
Manual Advanced Matching
You explicitly pass user data into fbq('init', ...) after login or form submit. Higher control, higher match rate.
<script>
fbq('init', 'YOUR_PIXEL_ID', {
em: '[email protected]', // pixel hashes for you
ph: '12125551234', // E.164 without '+'
fn: 'jane',
ln: 'doe',
ct: 'sanfrancisco',
st: 'ca',
zp: '94103',
country: 'us',
external_id: 'cust_4f9a'
});
fbq('track', 'PageView');
</script>
The pixel hashes these client-side before transmission. Do not pre-hash when using fbq('init', ...) — that produces double-hashing.
Step 5 — Event Match Quality (EMQ) Score
EMQ is Meta's 0–10 grade of how well your event payload identifies a real Meta user. It directly governs Advantage+ optimization quality and lookalike seed quality. Find it in Events Manager → Datasets → Overview → Event Match Quality column.
Tier interpretation
| Score | Tier | What it means | Optimization impact |
|---|---|---|---|
| 0–3 | Poor | Mostly unidentified users; only IP/UA or fbp | Algorithm flying blind; expect inflated CPA |
| 4–5 | OK | Some hashed PII but inconsistent | Borderline; common when only emails are sent |
| 6–7 | Good | Multiple match keys, but hashing/normalization gaps | Algorithm works; CPA mostly accurate |
| 8 | Great (target) | Email + phone + fbp + fbc + IP/UA + external_id, all clean | Meta strongly prefers this floor |
| 9–10 | Excellent | Plus consistent external_id, fb_login_id, or db/ge | Maximum optimization confidence |
Match keys ranked by impact
Highest-impact keys to push from EMQ 6 → 9:
- Hashed email (
em) — single biggest lever; Meta's graph is email-anchored. fbc(fromfbclid) — strongest attribution for ad-driven traffic. Capture, persist 90d, send on every event.fbp— always present if pixel ran on the same browser; never omit.- Hashed phone (
ph) — second-best identifier, especially for mobile-first audiences. external_id— your stable internal ID; ties together repeated sessions even when other identifiers rotate.client_ip_address+client_user_agent— required from server-side; raw, not hashed.fn/ln/ct/st/zp/country/db/ge— incremental but cheap once you have any of them.
Tactics:
- Enable Advanced Matching (manual or AAM) for all logged-in user states.
- Capture
fbclidon landing, persist to a first-party cookie, mirror into your DB on auth, and sendfbcfrom server with every event. - For e-commerce: pull email/phone from Stripe Customer at checkout — don't rely on the browser to forward it.
- Confirm hashing in the diagnostic: Test Events tool → click an event → "Original Event Data" should show your params; "User Information" should show
em(hashed) etc., not blank.
Step 6 — Aggregated Event Measurement (AEM)
AEM is Meta's iOS 14.5+ workaround for Apple's App Tracking Transparency (ATT). For users who opt out of ATT, Meta uses AEM to report aggregated, modeled conversions back to advertisers — bypassing the device-level signal Apple blocked.
What changed in 2025
The historical AEM rules required:
- 8 prioritized events per domain
- Manual ranking in the AEM tab in Events Manager
- Domain verification as prerequisite
- 24h reporting delay; only the highest-priority event per opt-out user counted
As of mid-2025, Meta removed the 8-event cap and the manual ranking requirement. The AEM tab was removed from Events Manager. AEM now automatically aggregates all eligible events without advertiser configuration. The reporting-delay and modeling characteristics remain.
What this means for you in 2026:
- You don't configure AEM anymore — there's nothing to click.
- Domain verification is still strongly recommended (for Business Manager domain claiming and to ensure your domain is the one credited), even though it's no longer technically required to opt into AEM.
- iOS conversion data is modeled. Don't compare iOS opt-out user CPAs 1:1 with Android — they're statistical estimates.
- Click-through attribution windows for opt-out iOS users remain shortened (1-day click default; 7-day click available where supported).
Step 7 — Deduplication
When the same event arrives from both Pixel (browser) and CAPI (server), Meta deduplicates so you don't double-count. The mechanism:
| Match field | Pixel side | CAPI side | Notes |
|---|---|---|---|
event_name | 4th positional arg of fbq('track', ..., ..., {eventID:...}) parsed name | event_name field | Case-sensitive: Purchase ≠ purchase |
event_id | eventID key in the 4th-arg object | event_id field | Must be identical string; case-sensitive |
Dedup window: events with matching event_name + event_id arriving within the dedup window are collapsed to one. Meta's documented window for Pixel↔CAPI dedup is 48 hours in most contexts; for the Original Event Data parameters used by offline events, the window can extend.
Shared event_id pattern
Generate the ID once on the server (or in a render-time variable), pass to the browser, and use the same value in the CAPI POST.
// crypto.randomUUID() on the server, embedded into the page render
const eventId = crypto.randomUUID(); // e.g. "5b8a..."
// Browser
fbq('track', 'Purchase', {
value: 29.99,
currency: 'USD',
content_ids: ['SKU-123'],
contents: [{ id: 'SKU-123', quantity: 1 }]
}, {
eventID: eventId // <-- this is the dedup key
});
// Server (later, from Stripe webhook)
await sendCapiEvent({
event_name: 'Purchase',
event_id: eventId, // <-- same value
// ... rest of payload
});
Common dedup failures
| Symptom | Cause | Fix |
|---|---|---|
Reported Purchase count is ~2x of true orders | event_id not shared, or one side missing it | Audit Test Events tool — both sources should show same Event ID |
| One source dedups, another doesn't | Casing mismatch (purchase vs Purchase) | Standardize on Purchase everywhere |
| CAPI fires before browser, no dedup | Server fired >48h after browser | Both should fire near-real-time; if backfilling, only send CAPI |
| Dedup works for some, not others | Race condition: server fires before page renders, no shared ID | Generate ID server-side at page-render and inject into HTML |
Step 8 — Integration Methods
Method A — Direct Pixel + Direct CAPI (Recommended for custom stacks)
Best for: custom Node/Python/Ruby backends, full control, no third-party dependencies.
Browser-side: install the Pixel base code
Add to <head> on every page (HTML or via your layout component):
<!-- Meta Pixel Code -->
<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_DATASET_ID');
fbq('track', 'PageView');
</script>
<noscript><img height="1" width="1" style="display:none"
src="https://www.facebook.com/tr?id=YOUR_DATASET_ID&ev=PageView&noscript=1"
/></noscript>
<!-- End Meta Pixel Code -->
Browser-side: fire conversion events with shared event_id
// At the top of the order-confirmation page render
const eventId = window.__META_EVENT_ID; // injected from server-rendered template
fbq('track', 'Purchase', {
value: 29.99,
currency: 'USD',
content_ids: ['SKU-123'],
contents: [{ id: 'SKU-123', quantity: 1, item_price: 29.99 }],
num_items: 1
}, {
eventID: eventId
});
Server-side: CAPI POST (Node, raw fetch)
The endpoint pattern — current as of Marketing API v22 (2026):
POST https://graph.facebook.com/v22.0/<DATASET_ID>/events?access_token=<TOKEN>
Content-Type: application/json
Full Node example:
// server/lib/meta-capi.js
import crypto from 'node:crypto';
const META_DATASET_ID = process.env.META_DATASET_ID;
const META_ACCESS_TOKEN = process.env.META_CAPI_ACCESS_TOKEN;
const API_VERSION = 'v22.0';
function sha256(value) {
if (!value) return undefined;
return crypto
.createHash('sha256')
.update(String(value).trim().toLowerCase())
.digest('hex');
}
function normalizePhone(phone) {
if (!phone) return undefined;
// Strip everything but digits; assume caller provides country code
return String(phone).replace(/\D/g, '');
}
export async function sendMetaEvent({
eventName,
eventId,
eventTime, // unix seconds
eventSourceUrl, // full URL of the page where event happened
actionSource, // 'website' | 'email' | 'app' | 'phone_call' | 'chat' | 'physical_store' | 'system_generated' | 'business_messaging' | 'other'
user,
customData,
testEventCode // pass during dev only
}) {
const payload = {
data: [
{
event_name: eventName,
event_time: eventTime || Math.floor(Date.now() / 1000),
event_id: eventId,
event_source_url: eventSourceUrl,
action_source: actionSource || 'website',
user_data: {
em: user.email ? [sha256(user.email)] : undefined,
ph: user.phone ? [sha256(normalizePhone(user.phone))] : undefined,
fn: user.firstName ? [sha256(user.firstName)] : undefined,
ln: user.lastName ? [sha256(user.lastName)] : undefined,
ct: user.city ? [sha256(user.city.replace(/\s/g, ''))] : undefined,
st: user.state ? [sha256(user.state)] : undefined,
zp: user.zip ? [sha256(user.zip.slice(0, 5))] : undefined,
country: user.country ? [sha256(user.country)] : undefined,
external_id: user.id ? [sha256(user.id)] : undefined,
client_ip_address: user.ip, // raw
client_user_agent: user.userAgent, // raw
fbc: user.fbc, // raw cookie value
fbp: user.fbp // raw cookie value
},
custom_data: customData
}
],
...(testEventCode ? { test_event_code: testEventCode } : {})
};
const url =
`https://graph.facebook.com/${API_VERSION}/${META_DATASET_ID}/events` +
`?access_token=${META_ACCESS_TOKEN}`;
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!res.ok) {
const errBody = await res.text();
throw new Error(`Meta CAPI ${res.status}: ${errBody}`);
}
return res.json();
}
Wire it to a Stripe webhook for the gold-standard Purchase source-of-truth:
// app/api/webhooks/stripe/route.js (Next.js App Router)
import { NextResponse } from 'next/server';
import Stripe from 'stripe';
import { sendMetaEvent } from '@/server/lib/meta-capi';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
export async function POST(req) {
const sig = req.headers.get('stripe-signature');
const buf = await req.arrayBuffer();
const event = stripe.webhooks.constructEvent(
Buffer.from(buf),
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
if (event.type === 'payment_intent.succeeded') {
const pi = event.data.object;
const meta = pi.metadata || {};
await sendMetaEvent({
eventName: 'Purchase',
eventId: meta.meta_event_id, // generated at checkout, written to PI metadata
eventTime: Math.floor(pi.created),
eventSourceUrl: meta.checkout_url,
actionSource: 'website',
user: {
email: pi.receipt_email,
phone: meta.phone,
id: pi.customer,
ip: meta.client_ip,
userAgent: meta.client_ua,
fbc: meta.fbc,
fbp: meta.fbp
},
customData: {
currency: pi.currency.toUpperCase(),
value: pi.amount_received / 100, // Stripe gives cents
order_id: pi.id,
contents: JSON.parse(meta.line_items || '[]')
}
});
}
return NextResponse.json({ received: true });
}
amount is in smallest currency unit (cents for USD). Divide by 100 before sending value to Meta. Sending 2999 instead of 29.99 will inflate your reported revenue 100x and corrupt ROAS bidding.Capturing browser context for the server
The server needs fbc, fbp, IP, and UA from the browser. Forward them via your checkout API or stash in user session:
// On checkout init in the browser
const fbp = document.cookie.match(/_fbp=([^;]+)/)?.[1];
const fbc = document.cookie.match(/_fbc=([^;]+)/)?.[1];
await fetch('/api/checkout/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
cartId,
eventId: crypto.randomUUID(), // generated client-side, stored server-side
fbp,
fbc
})
});
Server reads req.headers['x-forwarded-for'] (first IP) and req.headers['user-agent'], then writes fbp, fbc, IP, UA, and eventId into the Stripe PaymentIntent metadata so the webhook has them.
Method B — Conversions API Gateway (CAPIG)
Best for: clients without dev resources, or stacks where adding server code is impractical (legacy CMS, no backend).
CAPIG is Meta's hosted intermediary that auto-converts incoming Pixel events into CAPI events server-side. You install it as a self-hosted container (Meta provides AMI/Docker image) on AWS, GCP, or Azure, point your DNS at it, and it handles the rest.
| Attribute | Detail |
|---|---|
| Cost | CAPIG software is free from Meta. You pay for hosting (AWS EC2 ~$15–60/mo for typical volume) or use a managed provider. Managed providers like Stape charge $10/mo per pixel (pay-as-you-go) up to $100/mo for unlimited pixels. |
| Setup time | 30–90 min for Meta-hosted self-deploy; <15 min for managed provider |
| EMQ vs direct | Slightly lower than direct CAPI — gateway sees only what the browser exposed; no Stripe/CRM enrichment |
| When to use | Marketing-only sites, agencies running 5+ clients, no dev capacity |
| When not to use | E-commerce with refund/subscription logic — direct CAPI from your backend will always beat the gateway |
Method C — Partner Integrations
GTM Server-Side
- Web container: install Meta Pixel community template, configure with Dataset ID.
- Server container (sGTM in Cloud Run / App Engine): install Conversions API Tag (Meta's official template).
- Configure the tag with Dataset ID + Access Token.
- Map
event_idfrom your data layer to both the web pixel template (aseventID) and the server tag (asevent_id). - The server container forwards the request's IP and UA automatically; you must populate
fbp/fbcfrom cookies via custom variables.
Shopify
Shopify's native Meta integration (Sales channel → Facebook & Instagram) auto-installs both the Pixel and CAPI bridge. As of 2026 the integration:
- Sends
Purchase,AddToCart,ViewContent,InitiateCheckout,Searchserver-side automatically. - Auto-handles
event_iddedup. - Pulls hashed customer email/phone from the Shopify Customer record.
- Does not by default send
fbc/fbpreliably from Shopify Checkout Extensibility — verify in Test Events.
WooCommerce
Use the official Facebook for WooCommerce plugin. Configure Dataset ID + CAPI Access Token in plugin settings. The plugin handles dedup via event_id. Same caveat as Shopify: verify fbc/fbp propagation in Test Events.
Step 9 — Consent Mode for Meta
Limited Data Use (LDU) — CCPA/CPRA
LDU is Meta's signal that says "process this event for measurement only — no targeting, no Custom Audiences, no Lookalikes." It's how you stay compliant when a California user opts out under CCPA/CPRA.
Browser:
fbq('dataProcessingOptions', ['LDU'], 0, 0); // 0,0 = let Meta geo-detect
// or pin to a specific region:
fbq('dataProcessingOptions', ['LDU'], 1000, 1000); // 1000=US, 1000=California
Server (CAPI):
const payload = {
data: [{
event_name: 'Purchase',
// ...
data_processing_options: ['LDU'],
data_processing_options_country: 0,
data_processing_options_state: 0
// or country=1000, state=1000 to pin to US/California
}]
};
Behavior: when LDU is on, Meta still counts the conversion in your reporting but does not use it for ad targeting, Custom Audience seeding, or Lookalike modeling.
EU Consent (GDPR/ePrivacy)
For EU/EEA/UK users without consent, you should suppress event sending entirely — LDU is not a substitute for GDPR consent. Pattern:
if (!userHasMetaConsent) {
// Don't initialize the pixel; don't write _fbp; don't send CAPI.
return;
}
fbq('init', 'YOUR_DATASET_ID');
fbq('track', 'PageView');
For users who consent: standard tracking applies. A Consent Management Platform (OneTrust, Cookiebot, Iubenda) typically gates this. Meta does not currently offer a Google-Consent-Mode-equivalent "modeled conversions for non-consented users" mode — events without consent should not be sent.
fbp cookie writes and CAPI POSTs are considered "processing" under GDPR. A bare LDU flag does not make EU traffic compliant.Step 10 — Privacy Sandbox Compatibility
Short answer for 2026: not relevant.
In October 2025 Google announced the retirement of most Privacy Sandbox APIs — including the Attribution Reporting API and Topics API — across Chrome and Android. Adoption was low and the technology shipped well behind schedule. Google now keeps third-party cookies in Chrome (for now) and is repurposing the Attribution Reporting work in the W3C Private Advertising Technology Working Group.
What this means for Meta attribution:
- You don't need to integrate Attribution Reporting API for Meta in 2026.
- Third-party cookies are still alive in Chrome — but Safari ITP and Firefox ETP still block them, and that's still 30–40% of US web traffic.
- The dual-tracking stack (Pixel + CAPI) remains the correct architecture — the same one you'd use whether or not Privacy Sandbox shipped.
Step 11 — Verification & Debugging
Meta Pixel Helper (Chrome extension)
Install. Browse the site and confirm:
- Base code fires once on every page (
PageView). - Conversion events fire at the correct funnel stages.
eventIDis present in the event payload (it's the dedup key).- Advanced Matching parameters appear (in green) on events that should have them.
- No duplicate
Purchaseevents on a single page load.
Test Events tool
Events Manager → your Dataset → Test Events tab.
- Copy the test code (e.g.,
TEST12345). - Pass it as
test_event_codein your CAPI POST body, or append?test_event_code=TEST12345to the URL when triggering Pixel events from a test browser. - Within 30 seconds, both Pixel and CAPI events should appear in the Test Events stream.
- Click an event to inspect the Original Event Data (raw fields) and User Information (which match keys arrived).
// Pass test_event_code only in dev/staging
await sendMetaEvent({
eventName: 'Purchase',
eventId: 'evt_test_001',
// ...
testEventCode: process.env.NODE_ENV !== 'production'
? 'TEST12345'
: undefined
});
test_event_code in production. Test events do not flow into reporting or optimization — if you ship it on, your real conversions become invisible.Diagnostics tab
Events Manager → Dataset → Diagnostics. Surfaces:
- Missing required parameters (e.g.,
Purchasewithoutvalue/currency) - Hashing issues ("we received unhashed data" — fix immediately)
- Dedup mismatches
- Domain-verification status problems
- Drop-off in EMQ over time
Event Coverage tool
Events Manager → Dataset → Overview → Event Coverage — shows the share of each event coming from Pixel vs CAPI vs both. Healthy state: most events come from both. CAPI-only events flag where the browser pixel is being blocked (good signal that your CAPI investment is paying off). Pixel-only events for high-value funnel stages are a red flag — extend CAPI coverage.
Common issues table
| Issue | Likely cause | Fix |
|---|---|---|
| Events accepted but not optimized | Event name typo (purchase not Purchase) | Audit casing across Pixel + CAPI |
| EMQ stuck at 4–5 | Email sent unnormalized (mixed case / whitespace) | Lowercase + trim before SHA-256 |
Reported Purchase ~2x actual orders | Dedup not working — event_id not shared, or casing mismatch on event_name | Inspect Test Events: matching IDs on both events |
| Reported revenue 100x too high | Sending Stripe cents as value | Divide by 100 |
| iOS conversions not attributing | Domain not verified, or expecting same-day reporting on AEM | Verify domain; AEM has reporting delay; conversions are modeled |
fbc always missing | fbclid not persisted across pages | Capture on first landing, write to a long-lived first-party cookie |
| All CAPI events 400 | Wrong API version, malformed user_data arrays (must be arrays, not strings) | Validate against Server Event Parameters |
data_processing_options not respected | Sent as string 'LDU' instead of array ['LDU'] | Always array |
| Test events appear, real events don't | test_event_code left in production code | Gate behind env check |
Step 12 — 2026 Specifics: What Changed
The last 12 months of Meta attribution changes worth knowing:
| Change | Effective | Impact |
|---|---|---|
| AEM 8-event cap removed, AEM tab removed from Events Manager | Mid-2025 | No more event ranking. AEM auto-aggregates all eligible events. Old setup guides referencing the AEM tab are stale. |
| Marketing API v22.0 released | Late 2025 / early 2026 | Use v22.0 in CAPI endpoint URLs. v21 still works but is on deprecation track. |
| "Pixel" → "Dataset" rename completing in UI | Rolling through 2025–2026 | Same ID, new label. CAPI API still uses pixel_id field name in some endpoints; <DATASET_ID> in URL path. |
| CAPI weight increased in attribution | Throughout 2025–2026 | Server signals weighted more heavily than browser. EMQ ≥8 from CAPI is now table stakes for Advantage+ shopping. |
Click-to-WhatsApp ctwa_clid click ID parameter for CAPI | 2025+ | New click-ID for Click-to-WhatsApp ads; pass through your CRM/webhook into CAPI to attribute WhatsApp leads back to ad. |
Lead Ads CRM CAPI (server-side Lead/CompleteRegistration from CRM) | Maturing through 2025–2026 | Mark a CRM lead "qualified" → fire Lead to CAPI with lead_id from the original Meta Lead Ad. Optimization improves dramatically. |
| CAPI Gateway managed pricing standardizing | 2026 | Stape: ~$10/mo/pixel pay-as-you-go, ~$100/mo unlimited. Self-hosted is still free + your AWS bill. |
| Privacy Sandbox attribution APIs retired | Oct 2025 | No need to plan for Attribution Reporting API integration. |
Orbit MCP Integration
If your team uses Claude Code or Cursor for development, connect the Orbit MCP server to your AI assistant. The MCP exposes:
- This setup guide as a reference document
- Live Meta Events Manager diagnostics for your Dataset
- EMQ score lookups
- Test Events tool integration to validate implementations from chat
- CAPI payload validators that catch the issues in the table above before you hit the API
Ask your Orbit contact to get your MCP credentials.
Reference Links
Last updated: May 2026 · Maintained by Orbit (orbitllm.com)