Meta CAPI Deduplication Is Not Working
Deduplication requires the browser and the server to send an identical event_id and an identical event_name. Most broken setups get one of the two right.
Get My Free Audit →Are you seeing this?
- Events Manager reports roughly double the purchases your store recorded
- The deduplication or overlap section shows a low percentage of events being merged
- Reported ROAS looks excellent and does not survive reconciliation against your order table
- You configured event_id and the duplication continued anyway
- Meta shows a warning about duplicate events on your Purchase event
Why it happens
event_id is generated independently on each side.
The browser creates one value, the server creates another. Both are present, both are valid, and they will never match. This is the most common cause and the most misleading, because the configuration looks correct in every interface you can inspect.
event_name differs between the two sources.
Meta deduplicates on the pair of event_name and event_id. Purchase and purchase are not the same string. A mismatch here defeats deduplication even when the ID is shared perfectly.
event_id is present on one side only.
Usually the server side has it and the pixel does not, because the pixel was configured first and never revisited. An event without an ID cannot be deduplicated against anything.
The server event arrives outside the deduplication window.
Meta deduplicates events received within roughly 48 hours of each other. A batched or queued server-side pipeline that delivers events on a daily cycle can fall outside it, particularly around outages or retries.
Only one of the two sources is actually firing for some users.
Consent denials, ad blockers, and network failures mean the pixel fires for some visitors and not others. This is normal and is precisely why you run both — but it makes the deduplication rate look worse than it is, and it means the rate should never be expected to reach 100%.
How to diagnose it
Step 01 — Confirm duplication before chasing the cause.
Compare Meta's reported purchase count against your store's order count for an identical date range. A ratio near 2.0 means systematic double counting. A smaller excess means partial duplication, which usually points to cause 05 rather than a configuration error.
Step 02 — Inspect both payloads for one real order.
Use the Test Events tool and complete a purchase. Capture the event_id and event_name from the browser payload and from the server payload. Compare them character by character. Most cases are solved here.
Step 03 — Check the case of event_name.
Meta's standard events are capitalised — Purchase, AddToCart, InitiateCheckout. A server implementation sending lowercase will not deduplicate and may not map to the standard event at all.
Step 04 — Read the deduplication metrics rather than assuming.
Events Manager reports how many events were deduplicated. A configuration that looks correct but shows a near-zero deduplication rate is not working, and the interface will not tell you why.
Step 05 — Check when the server event arrives.
Look at your delivery pipeline. If events are queued and flushed on a schedule, confirm the lag is comfortably inside the deduplication window under normal conditions and during retries.
How to fix it
Generate the event_id once, server-side, and give the same value to both destinations. Never generate it twice.
/**
* Create or retrieve one event ID per order, stored so it survives
* page reloads, retries, and a delayed server-side send.
*/
function propulse_get_purchase_event_id( WC_Order $order ) {
$event_id = $order->get_meta( '_propulse_fb_event_id' );
if ( $event_id ) {
return $event_id;
}
$event_id = 'purchase_' . $order->get_id() . '_' . wp_generate_uuid4();
$order->update_meta_data( '_propulse_fb_event_id', $event_id );
$order->save();
return $event_id;
}Output the same value to the browser:
add_action( 'woocommerce_thankyou', 'propulse_fb_browser_purchase', 20, 1 );
function propulse_fb_browser_purchase( $order_id ) {
$order = wc_get_order( $order_id );
if ( ! $order ) {
return;
}
$event_id = propulse_get_purchase_event_id( $order );
// event_name must match the server payload exactly, including case.
$payload = array(
'event' => 'fb_purchase',
'event_name' => 'Purchase',
'event_id' => $event_id,
'value' => (float) $order->get_total(),
'currency' => $order->get_currency(),
);
echo '<script>window.dataLayer = window.dataLayer || [];';
echo 'window.dataLayer.push(' . wp_json_encode( $payload ) . ');</script>';
}And send the identical pair server-side:
$body = array(
'data' => array(
array(
'event_name' => 'Purchase', // identical string
'event_id' => $event_id, // identical value
'event_time' => $order->get_date_created()->getTimestamp(),
'event_source_url' => $order->get_checkout_order_received_url(),
'action_source' => 'website',
'user_data' => propulse_capi_user_data( $order ),
'custom_data' => array(
'currency' => $order->get_currency(),
'value' => (float) $order->get_total(),
),
),
),
);Three details that matter:
Storing the ID in order meta, rather than generating it inline, means a customer who refreshes the confirmation page gets the same ID rather than a new one — which would create a genuine duplicate that no deduplication can catch.
event_time uses the order creation timestamp, not the moment the request is sent. If your server-side delivery is delayed, the event still carries the correct time.
action_source: 'website' tells Meta the event originated on your site. Mismatched values between browser and server can prevent merging.
How to verify the fix worked
- Complete a test order and confirm the browser and server payloads carry an identical event_id and event_name
- Check the order meta in wp-admin — _propulse_fb_event_id should be present
- Refresh the confirmation page and confirm the ID does not change
- Check the deduplication rate in Events Manager after 24–48 hours
- Reconcile Meta's purchase count against your order count for a full week after the fix; the ratio should approach 1.0
When this needs more than a snippet
If IDs match and deduplication still fails, the cause is usually structural: events firing from more than two sources, a partner integration sending its own Purchase events, or a plugin duplicating what your custom implementation already does.
Tracing that means mapping every source sending to your pixel ID, which is part of our Meta CAPI implementation work.
Tell us about your setup. We respond within 24 hours.
Common
Questions
Deduplication happens as events are received, and matching events must arrive within roughly 48 hours of each other. Reporting takes longer to settle, so give it a day or two before judging the result rather than reading the first hours after a change.
Generally no. The two capture different signals — the pixel sees browser-side context and behaviour, the API sees confirmed server-side events. Running both with correct deduplication gives better coverage than either alone. Turning off the pixel to solve duplication is fixing the symptom by removing half your data.
You can, and it is simple and stable. The trade-off is that order IDs are sequential and guessable, which some teams prefer not to expose in a client-side payload. Prefixing a UUID as shown above avoids that while keeping the value stable per order.
Ready to Build Something That Works?
Book a free 30-minute strategy audit. No pitch deck, no pressure — just an honest look at your setup and what to fix first.
