MetaGuide

Meta Attribution Setup Guide

Complete dual-tracking stack for Meta: Pixel + Conversions API, Advanced Matching, EMQ optimization, AEM, Consent Mode, and 2026 deduplication rules.

May 4, 202627 min read

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.

LayerMechanismWhat it capturesWhat it misses
Browser-sideMeta Pixel (fbevents.js)Real-time UI events, fbp/fbc cookies, full page context, Advanced Matching auto-collected fieldsEverything blocked by ad blockers, Safari ITP, iOS ATT, Brave, Firefox ETP, server-only events (refunds, subscription renewals, offline conversions)
Server-sideConversions 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 Purchase pixel may never fire if the user closes the tab on the thank-you page.
  • Retries. A failed fetch from fbevents.js is 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 + hashed email + hashed phone + client_ip_address + client_user_agent from 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_id on 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.

  1. Go to business.facebook.com/events_manager2.
  2. Select your data source (Pixel/Dataset).
  3. Settings → copy the Dataset ID (15–16 digit number).

CAPI Access Token

Two ways to mint a token. Pick System User for production.

MethodWhen to useLifetime
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 buttonQuick prototyping, local testing only60 days

To create a System User token:

  1. business.facebook.com/settings/system-usersAdd → name it (e.g. orbit-capi).
  2. Assign assets: your Ad Account + Pixel/Dataset, with Manage Pixel permission.
  3. Generate New Token → select your app → scope: ads_management, business_management → set expiration to Never.
  4. 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:

MethodWhereProsCons
DNS TXT recordAdd facebook-domain-verification=<code> TXT record at rootMost robust — survives theme changes, deploys, CDN movesRequires DNS access; propagation 5min–48h
Meta tag<meta name="facebook-domain-verification" content="..." /> in <head> of root domainFast, works in most CMSLost if theme is replaced; must be on every page of root
HTML file uploadUpload provided .html to web rootNo DNS neededBrittle; 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.

Skipping domain verification is the single most common reason iOS attribution silently breaks. Even though AEM no longer requires it for event configuration (see Step 7), domain verification is still required to claim a domain in Business Manager and to retain control of which Business Account can run ads pointing at your site.

Step 2 — Standard Events Reference

Meta's algorithm optimizes against named standard events. Custom events work but get less optimization weight. Spelling matters.

Funnel StageEvent NameRequired ParamsRecommended ParamsWhen to fire
Page loadPageViewAuto-fired by base pixel; mirror server-side only if running pixel-less
Product detail pageViewContentcontent_ids, content_type (product), value, currencyUser lands on a PDP
Add to cartAddToCartcontents (req. for Advantage+ catalog)value, currency, content_idsUser clicks add-to-cart
Checkout startInitiateCheckoutvalue, currency, num_itemsUser enters checkout flow
Payment infoAddPaymentInfovalue, currencyCard/billing info captured
PurchasePurchasecurrency, value; contents or content_ids for Advantage+order_id, num_itemsOrder confirmed (fire from server on Stripe payment_intent.succeeded)
Account creationCompleteRegistrationvalue, currency, statusSignup form submitted
Lead captureLeadvalue, currency, lead_event_source, content_categoryLead form submitted
Subscription startSubscribevalue, currency, predicted_ltvPaid subscription begins
ContactContactUser initiates contact (call, email click)
SearchSearchcontents or content_ids for Advantage+search_string, value, currencySite search executed
Find locationFindLocationStore locator used
Customize productCustomizeProductConfigurator interaction
DonateDonatevalue, currencyDonation completed
ScheduleScheduleAppointment booked
Start trialStartTrialvalue, currency, predicted_ltvFree trial begins
Submit applicationSubmitApplicationApplication form submitted
Event names are case-sensitive. purchasePurchase. 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.
Official ref: Meta Pixel Standard Events. For Advantage+ Catalog Ads, 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.

ParamFieldHash?Normalization (before hash)Example raw → hashed
emEmailYesTrim, lowercaseJane@Example.com jane@example.com5e88...
phPhoneYesStrip all non-digits, include country code (E.164 without +)+1 (212) 555-1234121255512344d2c...
fnFirst nameYesTrim, lowercase, strip punctuationJanejane
lnLast nameYesTrim, lowercase, strip punctuationO'Neilloneill
ctCityYesTrim, lowercase, no spaces or punctuationSan Franciscosanfrancisco
stStateYesLowercase 2-letter ANSI code (US) or full lowercase elsewhereCAca
zpPostal codeYesFirst 5 digits only (US ZIP), trim/lowercase94103-123494103
countryCountryYesLowercase ISO 3166-1 alpha-2USus
dbDate of birthYesYYYYMMDD1985-04-1519850415 → hash
geGenderYesSingle lowercase char: m or fFemalef
external_idYour user/customer IDYes (recommended)Trim, lowercasecust_4f9a → hash
client_ip_addressUser IPNo — raw203.0.113.42
client_user_agentUser agentNo — rawMozilla/5.0 ...
fbcClick ID cookieNo — rawFormat: fb.<sub>.<ts>.<fbclid>fb.1.1714435200000.IwAR0Xy...
fbpBrowser ID cookieNo — rawFormat: fb.<sub>.<ts>.<rand>fb.1.1714435200000.987654321
subscription_idRecurring subscription IDNosub_1NxYz...
lead_idMeta Lead Ad lead IDNo123456789012345
fb_login_idFacebook user ID (logged-in users)No100012345678901
anon_idMobile app installation IDNoApp-generated UUID
madidMobile advertising ID (IDFA/GAID)NoLowercaseIDFA-XXXX-XXXX

Common hashing mistakes that nuke your EMQ:

  • Hashing the raw email without lowercasing first → Jane@example.com and jane@example.com produce different hashes; Meta will fail to match against its own internally-lowercased index.
  • Phone numbers with +, spaces, or () not stripped before hashing.
  • Sending country as USA instead of us.
  • Double-hashing — if your platform already hashes (Shopify, GTM templates), don't hash again.

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.

If you do manual Advanced Matching, disable AAM in Events Manager. Running both produces conflicts and duplicate parameter sets that can lower EMQ rather than raise it.

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

ScoreTierWhat it meansOptimization impact
0–3PoorMostly unidentified users; only IP/UA or fbpAlgorithm flying blind; expect inflated CPA
4–5OKSome hashed PII but inconsistentBorderline; common when only emails are sent
6–7GoodMultiple match keys, but hashing/normalization gapsAlgorithm works; CPA mostly accurate
8Great (target)Email + phone + fbp + fbc + IP/UA + external_id, all cleanMeta strongly prefers this floor
9–10ExcellentPlus consistent external_id, fb_login_id, or db/geMaximum optimization confidence

Match keys ranked by impact

Highest-impact keys to push from EMQ 6 → 9:

  1. Hashed email (em) — single biggest lever; Meta's graph is email-anchored.
  2. fbc (from fbclid) — strongest attribution for ad-driven traffic. Capture, persist 90d, send on every event.
  3. fbp — always present if pixel ran on the same browser; never omit.
  4. Hashed phone (ph) — second-best identifier, especially for mobile-first audiences.
  5. external_id — your stable internal ID; ties together repeated sessions even when other identifiers rotate.
  6. client_ip_address + client_user_agent — required from server-side; raw, not hashed.
  7. 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 fbclid on landing, persist to a first-party cookie, mirror into your DB on auth, and send fbc from 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.
Official ref: Dataset Quality API — programmatic EMQ scoring, accessible via the Marketing API.

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).
Many third-party blog posts and even some Meta partner docs still reference the old 8-event AEM setup. If a setup guide tells you to "rank your priority events in the AEM tab," it's stale. The tab no longer exists.

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 fieldPixel sideCAPI sideNotes
event_name4th positional arg of fbq('track', ..., ..., {eventID:...}) parsed nameevent_name fieldCase-sensitive: Purchasepurchase
event_ideventID key in the 4th-arg objectevent_id fieldMust 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

SymptomCauseFix
Reported Purchase count is ~2x of true ordersevent_id not shared, or one side missing itAudit Test Events tool — both sources should show same Event ID
One source dedups, another doesn'tCasing mismatch (purchase vs Purchase)Standardize on Purchase everywhere
CAPI fires before browser, no dedupServer fired >48h after browserBoth should fire near-real-time; if backfilling, only send CAPI
Dedup works for some, not othersRace condition: server fires before page renders, no shared IDGenerate ID server-side at page-render and inject into HTML

Step 8 — Integration Methods

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 });
}
Stripe 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.

AttributeDetail
CostCAPIG 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 time30–90 min for Meta-hosted self-deploy; <15 min for managed provider
EMQ vs directSlightly lower than direct CAPI — gateway sees only what the browser exposed; no Stripe/CRM enrichment
When to useMarketing-only sites, agencies running 5+ clients, no dev capacity
When not to useE-commerce with refund/subscription logic — direct CAPI from your backend will always beat the gateway

Method C — Partner Integrations

GTM Server-Side

  1. Web container: install Meta Pixel community template, configure with Dataset ID.
  2. Server container (sGTM in Cloud Run / App Engine): install Conversions API Tag (Meta's official template).
  3. Configure the tag with Dataset ID + Access Token.
  4. Map event_id from your data layer to both the web pixel template (as eventID) and the server tag (as event_id).
  5. The server container forwards the request's IP and UA automatically; you must populate fbp/fbc from 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, Search server-side automatically.
  • Auto-handles event_id dedup.
  • Pulls hashed customer email/phone from the Shopify Customer record.
  • Does not by default send fbc/fbp reliably 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.

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.

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.

2026 enforcement context: EU DPAs continue to actively pursue cases against advertisers transmitting Meta Pixel/CAPI events without prior consent. Both fbp cookie writes and CAPI POSTs are considered "processing" under GDPR. A bare LDU flag does not make EU traffic compliant.
Official ref: About Limited Data Use.

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.
  • eventID is present in the event payload (it's the dedup key).
  • Advanced Matching parameters appear (in green) on events that should have them.
  • No duplicate Purchase events on a single page load.

Test Events tool

Events Manager → your Dataset → Test Events tab.

  1. Copy the test code (e.g., TEST12345).
  2. Pass it as test_event_code in your CAPI POST body, or append ?test_event_code=TEST12345 to the URL when triggering Pixel events from a test browser.
  3. Within 30 seconds, both Pixel and CAPI events should appear in the Test Events stream.
  4. 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
});
Strip 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., Purchase without value/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 → OverviewEvent 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

IssueLikely causeFix
Events accepted but not optimizedEvent name typo (purchase not Purchase)Audit casing across Pixel + CAPI
EMQ stuck at 4–5Email sent unnormalized (mixed case / whitespace)Lowercase + trim before SHA-256
Reported Purchase ~2x actual ordersDedup not working — event_id not shared, or casing mismatch on event_nameInspect Test Events: matching IDs on both events
Reported revenue 100x too highSending Stripe cents as valueDivide by 100
iOS conversions not attributingDomain not verified, or expecting same-day reporting on AEMVerify domain; AEM has reporting delay; conversions are modeled
fbc always missingfbclid not persisted across pagesCapture on first landing, write to a long-lived first-party cookie
All CAPI events 400Wrong API version, malformed user_data arrays (must be arrays, not strings)Validate against Server Event Parameters
data_processing_options not respectedSent as string 'LDU' instead of array ['LDU']Always array
Test events appear, real events don'ttest_event_code left in production codeGate behind env check

Step 12 — 2026 Specifics: What Changed

The last 12 months of Meta attribution changes worth knowing:

ChangeEffectiveImpact
AEM 8-event cap removed, AEM tab removed from Events ManagerMid-2025No more event ranking. AEM auto-aggregates all eligible events. Old setup guides referencing the AEM tab are stale.
Marketing API v22.0 releasedLate 2025 / early 2026Use v22.0 in CAPI endpoint URLs. v21 still works but is on deprecation track.
"Pixel" → "Dataset" rename completing in UIRolling through 2025–2026Same ID, new label. CAPI API still uses pixel_id field name in some endpoints; <DATASET_ID> in URL path.
CAPI weight increased in attributionThroughout 2025–2026Server 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 CAPI2025+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–2026Mark a CRM lead "qualified" → fire Lead to CAPI with lead_id from the original Meta Lead Ad. Optimization improves dramatically.
CAPI Gateway managed pricing standardizing2026Stape: ~$10/mo/pixel pay-as-you-go, ~$100/mo unlimited. Self-hosted is still free + your AWS bill.
Privacy Sandbox attribution APIs retiredOct 2025No 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.

ResourceURL
Conversions API Overviewhttps://developers.facebook.com/docs/marketing-api/conversions-api
Get Startedhttps://developers.facebook.com/docs/marketing-api/conversions-api/get-started/
Best Practiceshttps://developers.facebook.com/docs/marketing-api/conversions-api/best-practices/
Customer Information Parametershttps://developers.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters/
Server Event Parametershttps://developers.facebook.com/docs/marketing-api/conversions-api/parameters/server-event/
Custom Data Parametershttps://developers.facebook.com/docs/marketing-api/conversions-api/parameters/custom-data/
fbp and fbc Parametershttps://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc
Deduplication Guidehttps://developers.facebook.com/docs/marketing-api/conversions-api/deduplicate-pixel-and-server-events/
Dataset Quality API (EMQ)https://developers.facebook.com/docs/marketing-api/conversions-api/dataset-quality-api/
Conversions API Gatewayhttps://developers.facebook.com/docs/marketing-api/gateway-products/conversions-api-gateway/
Meta Pixel Referencehttps://developers.facebook.com/docs/meta-pixel/reference
Meta Pixel — Advanced Matchinghttps://developers.facebook.com/docs/meta-pixel/advanced/advanced-matching/
About Hashing Customer Informationhttps://www.facebook.com/business/help/112061095610075
About Limited Data Usehttps://www.facebook.com/business/help/1151133471911882
About Aggregated Event Measurementhttps://www.facebook.com/business/help/721422165168355
Events Managerhttps://business.facebook.com/events_manager2
Domain Verification (Business Settings)https://business.facebook.com/settings/owned-domains
Meta Pixel Helper (Chrome)https://chromewebstore.google.com/detail/meta-pixel-helper/fdgfkebogiimcoedlicjlajpkdmockpc

Last updated: May 2026 · Maintained by Orbit (orbitllm.com)

Meta Attribution Setup Guide — Orbit