CAPI has been the standard answer to iOS 14.5, cookie loss, and ad blockers for four years now. Every mid-sized advertiser has it enabled — through Shopify's native integration, WooCommerce plugins, GTM server-side, or a custom backend. And yet the number one finding in our Meta audits in 2026 is still: CAPI is on, and it isn't helping.
The reason is almost never that CAPI “doesn't work.” It works. The reason is that Meta expects Pixel and CAPI to be two views of the same event, tied together by an ID both channels agree on. When the ID is missing, mismatched, or generated fresh on each side, Meta treats them as separate events, panics about duplicates, and drops the one with lower match quality — which is almost always the browser-side pixel that had the actual click IDs.
What Meta Actually Sees Without Dedup
A single purchase generates two events. One from the pixel in the browser, one from your server via CAPI. Meta receives both. Without a shared identifier, Meta's pipeline runs a fuzzy match on user_data — email, phone, IP, user agent, fbp, fbc. If it can guess these are the same person within a small time window, it deduplicates. If it can't, it either counts twice or throws one away.
| What you send | What Meta stores | Attribution effect |
|---|---|---|
| Pixel only | Whatever survived iOS 14.5 / ad-block / consent (roughly 40–60%) | Under-reports 30–60% depending on region and device mix |
| CAPI only | Full server event, no fbc/fbp unless captured server-side | Great volume, terrible match quality — attribution drops off |
Pixel + CAPI, no event_id | Both events, best-effort fuzzy dedup on user_data | Meta silently drops ~50% of one channel to protect the other |
Pixel + CAPI, matching event_id | Single canonical event with combined signal | Match quality jumps, EMQ 8+/10, attribution stabilises |
That third row is where most accounts sit and don't know it. Ads Manager looks fine. Events Manager shows both channels green. The Match Quality column is where the pattern shows up — it's stuck at 4–6 out of 10 even though CAPI is delivering full user_data. That gap is the dedup silently failing.
The event_id Contract Nobody Actually Reads
Meta's dedup rule is boringly specific. For any two events with the same event_name and within roughly 24 hours, if they share the sameevent_id, Meta treats them as one event and prefers the one with higher match quality. If either side is missing the ID, or the two IDs don't match character-for-character, Meta will fuzzy-match on user_data — and fuzzy matching in 2026 is worse than most teams believe.
The trap is that pixel and CAPI use different field names for the same concept. Pixel calls it eventID (camelCase). CAPI calls it event_id (snake_case). Every serialisation layer between GTM, WooCommerce, Shopify, and your Node backend has to preserve one, translate to the other, and hand the same UUID string to both channels. Miss any one hop and dedup is dead.
event_id strings — Shopify was generating one, the proxy another. Meta was dropping half. Fix took two hours: make the proxy read Shopify's event_id from the request payload instead of generating its own. Match Quality climbed to 8.7 in three days; reported purchases in Ads Manager rose 34%.How to Detect the Dedup Gap in 5 Minutes
Business.facebook.com → Data Sources → your dataset → Test Events. Paste your test URL, load the site, and complete a purchase or lead form. Two rows should appear for the same conversion: one labelled Browser, one labelled Server.
Both events must show the same string under Event ID. If one is missing, if the two differ, or if one shows a UUID and the other a timestamp — dedup is failing right now. Meta will still receive both events but will not merge them.
Events Manager → Overview → your dataset → “Deduplication for connections” card. When dedup is healthy, this card shows a green bar with a percentage of successfully deduplicated events, usually 85%+. If the card shows “No deduplicated events detected” or a number under 40%, the contract is broken.
Same Overview tab, per event, per source. CAPI without matching Pixel dedup will show EMQ 4–6. With dedup working, CAPI EMQ climbs to 7.5–9 because Meta enriches server user_data with the browser's fbc and fbp.
The whole test takes five minutes. It's the single most impactful diagnostic in a Meta audit, and it's the one that never gets run because “CAPI is installed” is treated as the finish line.
The Four Failure Modes We Actually See
1. Shopify native + third-party CAPI both firing
Shopify's Meta app installs its own pixel and CAPI once the dataset is connected. If you also have a GTM pixel or a Stape CAPI, you now have four streams for one purchase and zero shared event_id. Pick one source of truth per event. Uninstalling the Shopify native pixel in favour of GTM is fine; running both is not.
2. GTM web pixel + GTM server-side CAPI, but IDs generated separately
A very common mid-market setup. The web GTM tag generates eventID = crypto.randomUUID(). The server GTM tag generates its own event_id. Fix: generate the UUID on the browser, push it into the dataLayer, and forward it to server GTM in the request payload so both tags use the identical string.
3. WooCommerce plugin sending CAPI without pushing the ID to gtag
Most Woo CAPI plugins (PixelYourSite, Meta Pixel for WordPress) create their own event ID for the server event but never inject it into the browser pixel config. In the plugin settings there's usually a “Advanced — deduplication” toggle that must be turned on and paired with an fbq_eventID parameter in the pixel init. Off by default in every install we've seen this year.
4. Consent bridge or CMP delaying the pixel
If your CMP defers the pixel until after consent is granted but CAPI fires server-side unconditionally, the pixel might not fire at all for the ~20% of users who never engage the banner. Dedup is fine on paper but Meta only sees the CAPI half. This is a Consent Mode problem in Meta's dialect — the answer is the same as the GA4 Consent Mode fix: an explicit bridge from the CMP's accept event to the pixel init.
The Fix, End-to-End
eventID. Pass it to the pixel as the third argument of fbq('track', 'Purchase', {...}, { eventID: '...'}). Forward it to your server (via GTM, a hidden field, or the order object) and use it as event_id in the CAPI payload. Same string in both. Character-for-character.1. Pick where the ID is born
For e-commerce, generate on the browser at the moment the purchase completes and store on the order object. For lead-gen, generate on the browser at form submit and send it up with the form payload. For subscription products with webhook confirmations, generate on the webhook and inject into the thank-you page. One source per event.
2. Send it via Pixel — the exact syntax
Meta's pixel takes an optional fourth object with camelCase eventID. If you use GTM's Meta template, there's a “Event ID” field near the bottom — it maps to this argument. Never put the ID in the event data object itself; Meta ignores it there.
3. Send it via CAPI — the exact field name
CAPI takes event_id (snake_case) as a top-level field on each event in the data array, not nested inside user_data or custom_data. Getting this wrong is the most common serialisation bug we see — the ID is present in the payload but Meta doesn't read it.
4. Push fbc and fbp server-side too
Dedup alone isn't enough. Send the browser's _fbc cookie (the Facebook click ID) and _fbp cookie (the browser ID) with the CAPI event under user_data. Without fbc, CAPI can't confirm the click that started the session. Without fbp, Meta loses cross-session linkage. Both are required to reach EMQ 8+.
5. Hash the PII fields correctly
Email, phone, first name, last name, city, state, zip must be SHA-256 hashed lowercase before sending. Meta rejects unhashed PII silently — the event lands but user_data enrichment fails. Every mainstream CAPI library handles this; custom implementations often skip it. Verify in Test Events: hashed fields appear as opaque strings, unhashed appear as plain text with a warning.
What Match Quality Actually Tells You
Event Match Quality (EMQ) is Meta's proxy for how well it can attribute a server event to a real user. The scale runs 0–10. Most brands running CAPI without dedup sit at 5–6 and think that's normal. It isn't.
| EMQ | What it means | Attribution impact |
|---|---|---|
| 0–4 | Only email or only IP being matched | Meta treats event as unattributable; bidding degrades |
| 5–6 | Hashed email + phone, no fbc/fbp | Fuzzy dedup, ~50% of duplicates dropped, moderate under-report |
| 7–8 | Full user_data with fbp, sometimes fbc | Reliable dedup, Advantage+ audiences work well |
| 8.5–10 | All fields hashed + both cookies + Advanced Matching | Attribution stable, iOS gap largely closed via modeling |
We treat EMQ 7.5 as the minimum green threshold in audits. Below that, no Advantage+ campaign or Advanced Matching setting will save the numbers. Above that, Meta's modeling recovers most of the iOS/consent gap.
Diagnosis Checklist
Why This Matters More in 2026 Than 2022
In 2022, the iOS 14.5 fallout was fresh and Meta's modeling was young. A broken CAPI setup meant losing 20–30% of attribution. In 2026, cookie loss is worse, consent friction is universal, and Meta's AAA (Advanced Analytics Attribution) leans much harder on server signal to allocate spend across Advantage+ audiences. A broken dedup contract today means:
- Advantage+ audiences never fully form because signal is fragmented
- Value optimisation stops working — Meta doesn't trust the value payload
- Retargeting reach degrades because
fbpmapping is inconsistent - CBO reallocations flip weekly on noise instead of signal
The 5-minute test above is the fastest ROI in a Meta audit. It's also the first thing we check when a client says “CAPI is installed but Ads Manager still under-reports.” The answer is almost always the dedup key, and the fix is almost always four hours of implementation for a 20–40% recovery of reported conversions.
If your GA4 attribution is also under-reporting, that's a separate but related problem — start with the Consent Mode v2 gap or see how we approach a full paid-media rebuild in conversion tracking audits.