Installing Meta Pixel is the easy part. The real work is making sure Meta can map the events Pixel collects to the right user. That comes down to how Advanced Matching is delivered, how Conversions API dedup is wired, and how the fbc/fbp cookie lifecycle is managed. This post pulls those three pieces into a single reference.
For Pixel standard events, parameter shapes and Events Manager setup, see the Meta Pixel events and standard events post. This post adds the user matching layer on top of that one.
What Advanced Matching Is and When It Kicks In
When Pixel fires an event, Meta wants to map it to a user. If fbp cookie and an active Facebook session exist in the browser, the match is already strong. But under ad-block, Safari ITP, iOS 17 LTP and not-logged-in scenarios Meta may have no signal. Advanced Matching kicks in here: hashed user parameters (em, ph, fn, ln, ct, st, zp, db, ge, country, external_id) are sent alongside the event, and Meta matches them to its own user base1.
Three operating modes exist:
| Mode | Where data comes from | Who hashes | Typical use |
|---|---|---|---|
| Automatic Advanced Matching (AAM) | Form inputs (login, signup, checkout) in the browser | Meta | Quick setup, no GTM template |
| Manual Advanced Matching | fbq('init', PIXEL_ID, {em, ph, ...}) or fbq('track', EVENT, params, {eventID, user_data}) | Developer | GTM, Shopify customer object, headless storefront |
| CAPI customer_data | Server-side event payload | Developer | Server-side tracking, sGTM, direct API |
AAM and Manual Advanced Matching can run on the same browser event; if parameters collide, AAM wins. To take full control, turn AAM off and rely on Manual Advanced Matching.
Parameter Normalization Rules
The most critical step. The same email normalized differently produces two different hashes, and matching fails. Meta’s official parameter rules2:
| Parameter | Normalization | Example |
|---|---|---|
em (email) | trim, lowercase | Ali@Site.COM → ali@site.com |
ph (phone) | strip symbols/letters, remove leading zeros, country code required | +90 (555) 123-4567 → 905551234567 |
fn (first name) | lowercase, no punctuation, UTF-8 (preserve diacritics) | Ayşe → ayşe |
ln (last name) | lowercase, no punctuation, UTF-8 | Yılmaz → yılmaz |
ge (gender) | single char m or f | Female → f |
db (date of birth) | YYYYMMDD | 15/03/1990 → 19900315 |
ct (city) | lowercase, no spaces, no punctuation, UTF-8 | İstanbul → istanbul; New York → newyork |
st (state) | hashing required; format not fully specified by Meta. US example shows 2-letter abbreviation (ca, ny); for non-US, no official guideline. In practice, a stable short code (e.g. plate code 34 for İstanbul) is common. | US California → ca |
zp (zip) | lowercase, no spaces, no dash. US: first 5 digits. UK: area-district-sector concatenated (e.g. m11ae). Other countries: local format. | US 94035, UK M1 1AE → m11ae |
country | ISO 3166-1 alpha-2, lowercase | Turkey → tr |
external_id | hashing recommended (not required); must be consistent across channels (same value on Pixel and CAPI) | customer_18745 → SHA-256 |
Parameters not hashed: fbc, fbp, client_ip_address, client_user_agent, subscription_id, lead_id. These are sent plain.
// Manual Advanced Matching, lowercase + trim then SHA-256
async function sha256(s) {
const buf = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(s),
);
return Array.from(new Uint8Array(buf))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
const email = "Ali@Site.COM".trim().toLowerCase();
const phone = "+90 (555) 123-4567".replace(/[^0-9]/g, "").replace(/^0+/, "");
fbq(
"track",
"Purchase",
{ value: 199.9, currency: "TRY", content_ids: ["SKU-123"] },
{
eventID: "ord_2026050100012",
user_data: {
em: await sha256(email),
ph: await sha256(phone),
},
},
);
Diacritic preservation is critical for Turkish names and cities: ayşe and
ayse produce different hashes. If your frontend and backend apply different
rules, Pixel/CAPI dedup will map events to different users.
fbc and fbp Cookie Lifecycle
The two serve different roles. fbp acts as the identity anchor on Meta’s side: it ties subsequent events from the same browser to the same session for 90 days, even when the user is logged out of Facebook. When email/phone aren’t available and fbclid has been stripped from the URL, fbp is often the only anchor Meta has left. Per the official External ID documentation, when external_id isn’t sent Meta uses fbp in its place3; without event_id, Meta falls back to heuristic dedup using fbp to relate Pixel and CAPI events; and fbp plays the role of a browser node in Website Custom Audience and Lookalike construction.
fbc is the attribution signal: it comes from an ad click, says “this visit arrived from an ad”, and lets Meta tie the conversion to the right campaign. fbp is identity, fbc is attribution. Sending both in the CAPI payload is standard practice; sending only fbc weakens cross-session linkage, sending only fbp loses attribution.
fbp (Facebook Browser ID) is a random identifier generated on first visit. fbc (Facebook Click ID) is generated from the fbclid URL parameter when the user lands from an ad click4. Their formats:
fbp = fb.<subdomain_index>.<creation_timestamp_ms>.<random_number>
fbc = fb.<subdomain_index>.<creation_timestamp_ms>.<fbclid>
Per Meta’s official definition, subdomain_index is 0 for com, 1 for apex (example.com), 2 for subdomain (www.example.com). On the server when no cookie exists, use 14. The fbclid value is case-sensitive; do not apply lower/upper transformations.
The Pixel library writes these cookies automatically, but two structural problems exist:
1. iOS 17 Link Tracking Protection. With iOS 17, Safari, Mail and Messages can strip known tracking parameters (including fbclid) from URLs by default5. Result: even if the user clicks an ad, no fbclid arrives at the page, no fbc cookie is generated, attribution is lost on Meta’s side. iOS 26 introduced Advanced Fingerprinting Protection, which extends this regime by blocking technical fallbacks used when cookies fail; the two features are distinct.
2. Safari ITP 7-day cap. Safari caps the TTL of client-side Set-Cookie cookies at 7 days6. Since fbp/fbc are written client-side by the Pixel library, they fall under this cap.
The practical fix for both is the same: persist the cookie server-side on first hit and replay it on subsequent events. Meta’s official guideline is to set _fbc only when4:
- the
_fbccookie does not exist andfbclidis present in the URL, OR - the URL
fbcliddiffers from the value after the last.in the existing_fbccookie.
// Edge worker or Astro middleware
const url = new URL(request.url);
const fbclid = url.searchParams.get("fbclid");
const existingFbc = parseCookie(request.headers.get("cookie"))._fbc;
const existingFbclid = existingFbc ? existingFbc.split(".").pop() : null;
if (fbclid && fbclid !== existingFbclid) {
// subdomain_index = 1 when generated server-side without a cookie
const fbc = `fb.1.${Date.now()}.${fbclid}`;
response.headers.append(
"Set-Cookie",
`_fbc=${fbc}; Path=/; Max-Age=7776000; HttpOnly; Secure; SameSite=Lax`,
);
}
Server-side HttpOnly cookies are not subject to the ITP 7-day cap. CAPI event payloads read fbc from the server cookie. The same pattern works for fbp; on first page view, generate fb.1.<now>.<random>.
CAPI Deduplication via event_id
Pixel and Conversions API send the same event through two channels to recover browser losses. But Meta must understand it’s a single event, not double-count it. That’s the job of event_id matching7.
Official rule: identical event_name and event_id arriving via Pixel and CAPI within 48 hours are counted as one event; older arrivals are treated as separate events. When server and browser events arrive at approximately the same time (within 5 minutes), Meta favors the browser/app event8. So keep CAPI delivery close to Pixel firing time (ideally within minutes).
// Browser side (Pixel)
const eventId = `ord_${orderId}`; // fixed value from backend
fbq(
"track",
"Purchase",
{ value, currency, content_ids },
{ eventID: eventId },
);
// Server side (CAPI), same eventId
// For website events, action_source and event_source_url are MANDATORY
await fetch(
`https://graph.facebook.com/v25.0/${PIXEL_ID}/events?access_token=${CAPI_TOKEN}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
data: [
{
event_name: "Purchase",
event_time: Math.floor(Date.now() / 1000),
event_id: eventId,
action_source: "website",
event_source_url: pageUrl,
user_data: {
em: [hashedEmail],
ph: [hashedPhone],
client_ip_address: clientIp,
client_user_agent: clientUa,
fbp,
fbc,
},
custom_data: { value, currency, content_ids },
},
],
}),
},
);
For website CAPI events, client_user_agent, action_source and event_source_url are required9. Missing any one invalidates the event. Pixel sends client_user_agent automatically; on the server it must be added manually.
Critical event_id rules:
- Use stable identifiers from the backend (order id). A random UUID generated separately in each channel breaks dedup.
- In Events Manager > Test Events, Pixel and CAPI events should be marked “Deduplicated”. If you see “Server” or “Browser” alone, dedup didn’t fire.
- For session events like PageView, page-load timestamp + tab id is enough. For Purchase, Lead, CompleteRegistration use a backend record id.
When using test_event_code for Test Events, remember to remove it from
production payloads. Per Meta’s official note: events sent with
test_event_code are not dropped; they flow into Events Manager and are used
for targeting and ads measurement10. So data sent “as a test” can pollute
real audience data.
EMQ: Diagnostic, Not ROAS Proxy
Event Match Quality (EMQ) is Meta’s 0-10 score for how matchable the events you send are11. It’s a weighted aggregate of parameters. Meta does not publish exact weights, but practitioners observe that high-determinism parameters like email and phone contribute substantially more than zip or city.
The common misreading is “I raised EMQ to 8, my campaign performance will improve”. EMQ is not a direct performance metric, because:
- By definition EMQ is a match quality signal, not a coverage signal. It scores how well events that reached Meta map to a known user; it does not recover events that were never sent. If Pixel was blocked by adblock and you start sending the same event via CAPI, EMQ goes up — but the win comes from the added event flow, not from the EMQ figure itself.
- Matching is performed against the user base Meta already knows, so for a brand-new user the match can still fail even when parameters are complete (i.e. EMQ is high). This follows from how the matching mechanism is defined rather than from any explicit doc statement.
A standalone vanity-metric critique of EMQ is in a separate post (cluster spoke, in preparation). The goal here is the mechanics of raising EMQ correctly.
Practical steps:
- Send the email the moment it’s captured. Login, signup, newsletter, cart email — every input should attach
emto the event. Don’t wait for checkout. - Force phone to E.164. Add the country code as a default in the frontend if missing, remove leading zeros, strip whitespace, parentheses, dashes.
- Persist fbc/fbp server-side. See the cookie pattern above.
- Add external_id. For logged-in users, send the same (hashed) customer_id through Pixel and CAPI. Strong for cross-device matching. Note: external_id is not visible in Test Events; it shows up only in Pixel Helper under Advanced Matching Parameters Sent.
- Add client_ip_address and client_user_agent on CAPI. Pixel sends them automatically; CAPI requires manual inclusion, and
client_user_agentis required for website events.
Consent Mode: Don’t Mix With Google’s
A common mistake is assuming Google’s Consent Mode v2 and Meta’s consent mechanism are the same. They are not.
Consent on Meta’s side runs at two levels12:
// 1. Pixel-level consent (browser)
fbq("consent", "revoke"); // default before banner is shown
// User accepts
fbq("consent", "grant");
// 2. CAPI-level Limited Data Use (server)
// For US state laws, add data_processing_options to payload
{
"data_processing_options": ["LDU"],
"data_processing_options_country": 0,
"data_processing_options_state": 0
}
Practical rules:
- For EU traffic, neither Pixel nor CAPI should fire before consent is granted. Meta’s LDU does not satisfy EU GDPR; LDU is for US state laws like CCPA13.
- After the consent banner, fire
fbq('consent', 'grant')first, then trigger Pixel events. Calling track before grant drops events. - Keeping consent state in sync between Pixel and CAPI is easier if you include a consent timestamp in the CAPI payload, useful for debug.
For the broader server-side and attribution implications of consent, see Post-consent ad measurement and Browser storage and cookie architecture.
Multiple Pixel ID Scenarios
Running multiple Pixel IDs is rarely a problem on its own; the problem is when it’s unclear which Pixel an event should target. Meta’s official guidance14:
“Using the
trackfunction on pages that have multiple Pixels initialized could produce over-firing or unexpected behavior and should be applied only in specific situations.”
fbq("init", PIXEL_AGENCY_A);
fbq("init", PIXEL_BRAND_OWN);
// PageView fires on both — desired
fbq("track", "PageView");
// Purchase only on the brand pixel
fbq(
"trackSingle",
PIXEL_BRAND_OWN,
"Purchase",
{ value, currency },
{ eventID },
);
// Custom event only on the agency pixel
fbq("trackSingleCustom", PIXEL_AGENCY_A, "AgencyKPI", { ... });
trackSingle and trackSingleCustom scope an event to a specific Pixel ID. Without them, a track call fires on every initialized Pixel ID; this is what causes third-party integrations (e.g. the Shopify Facebook & Instagram app) to leak their events onto other Pixels. A dedicated post on this Shopify-specific bug and its diagnosis is here: Shopify F&I app multi-pixel duplicate event.
Validation: Test Events and Pixel Helper
Before going to production, validate with two tools:
| Tool | Shows | Where to look |
|---|---|---|
| Meta Pixel Helper (Chrome extension) | Browser-side Pixel events, parameters, errors; AAM-captured data; external_id visible | Live event flow, Advanced Matching Parameters Sent |
| Events Manager > Test Events | Pixel + CAPI events, dedup status; external_id not visible | ”Server”/“Browser”/“Deduplicated” markers |
If an event shows up as Server or Browser alone (not Deduplicated) in Test Events, dedup is broken. Common causes:
- Pixel and CAPI generated different event_ids.
- event_time differs by more than 5 minutes between channels (Meta favors browser when tie-breaking).
event_namediffers (PurchasevspurchasevsCompletePayment).- The 48-hour dedup window has passed.
After validation, walk through Events Manager Diagnostics warnings (mismatched currency, missing parameters, unmapped content_ids) and clear them. A perfectly empty Diagnostics is rare; the goal is no Critical-level warnings remaining.
Server-Side Transition and Related Posts
Advanced Matching and CAPI dedup were written assuming Pixel runs in the browser. As traffic grows and bot/adblock losses scale, moving server-side becomes necessary. For hosting decisions, see sGTM Hosting Decision Matrix. For non-GTM Pixel/CAPI flow approaches, see Different Approaches to Event Data Tracking. For the 2026 attribution window shrink and a recovery layer, see Meta Attribution Window: BigQuery Recovery Layer.
Advanced Matching normalize/hash pipeline, event_id dedup architecture, server-side fbc/fbp persistence and EMQ diagnostics. Meta tracking review tailored to your stack.
Get in touchFootnotes
- About Advanced Matching for Web. Meta for Developers ↩
- Customer Information Parameters. Meta Conversions API ↩
-
External ID — fbp fallback. Meta Conversions API — “Event includes
fbp, but notexternal_id: We usefbpasexternal_idand try to find a match.” ↩ - ClickID and the fbp/fbc Parameters. Meta Conversions API ↩ ↩2 ↩3
- Meta Pixel: Setup Guide for Facebook Ads — iOS privacy timeline. Shopify Blog — Shopify blog clarifies the iOS 17 LTP / iOS 26 Advanced Fingerprinting distinction; verify against Apple’s official iOS release notes. ↩
-
Tracking prevention in Safari. WebKit — framework documentation describing ITP’s day-cap on client-side
Set-Cookiecookies. ↩ - Deduplicate Pixel and Server Events. Meta Conversions API ↩
- Server Event Parameters — event_name dedup window. Meta Conversions API — “match between events sent within 48 hours of each other”. ↩
- Conversions API Parameters Index — required for website events. Meta ↩
-
Using the Conversions API — Test Events tool warning. Meta — “Events sent with
test_event_codeare not dropped. They flow into Events Manager and are used for targeting and ads measurement purposes.” ↩ - Event Match Quality (EMQ). Meta Business Help Center ↩
- About Cookie Settings for Meta Pixel. Meta Business Help Center ↩
- Limited Data Use. Meta Business Help Center ↩
-
Selective event tracking with multiple Pixels. Meta Pixel Advanced — direct documentation of
trackSingleandtrackSingleCustom. ↩
- 01 Advanced Matching parameters must be normalized per Meta's format rules before sending; otherwise the hash differs and matching fails.
- 02 When Pixel and CAPI run in parallel, event_id must be generated from a single source on every event; if Pixel and CAPI produce different ids, both events are counted separately. Dedup window is 48 hours.
- 03 fbc cookie can become inaccessible client-side under iOS 17 LTP and Safari ITP; persist it server-side on first hit.
- 04 EMQ is a diagnostic, not a ROAS proxy. Use CPA delta to validate the impact of EMQ improvements.
- 05 Consent Mode v2 is Google's framework. On Meta's side, consent is managed via
fbq('consent', 'grant'|'revoke')and Limited Data Use on CAPI.
+ What's the difference between Advanced Matching and Conversions API customer_data?
Both use the same user parameter set (em, ph, fn, ln, ct, st, zp, db, ge, country, external_id, fbc, fbp, client_ip_address, client_user_agent). The difference is the channel: Advanced Matching is sent via Pixel in the browser, CAPI customer_data via the server. When the same event is sent through both, event_id matching is mandatory.
+ Should I hash the email parameter or does Meta do it automatically?
If Automatic Advanced Matching (AAM) is on, Meta hashes the data captured in the browser. If you use Manual Advanced Matching or CAPI, you must apply lowercase + trim + SHA-256 yourself. fbc, fbp, client_ip_address, client_user_agent and external_id are not hashed.
+ How long are fbc and fbp cookies retained?
Meta's default is 90 days for both, refreshed on every interaction. Under Safari ITP and iOS 17 Link Tracking Protection, client-side fbc is not durable; persisting it server-side on the first hit and replaying it on subsequent events recovers attribution loss.
+ What EMQ score should I aim for?
Meta officially labels 6.0-7.9 as Good and 8.0+ as Great (under 4 is Poor, 4-5.9 is OK). EMQ is not a direct campaign performance metric. A higher EMQ helps Meta map an event it already received to a user; it does not bring back events that were never sent. To tie EMQ to performance, compare it against CPA delta directly.
+ Can I deduplicate Pixel and CAPI without event_id?
You can attempt heuristic dedup using event_name + event_time + fbp + content parameters, but it's unreliable. Meta's official method is event_name + event_id. The same event_id must be generated from a single source, sent through both channels, and reach Meta within a 48-hour window.