Meta Conversions API deduplication: generate the event_id once, then pass it
If you send purchases to Meta from both the browser (Pixel) and your server (Conversions API), Meta receives every order twice. The only thing stopping it from counting both is a shared identifier. Meta deduplicates whe
If you send purchases to Meta from both the browser (Pixel) and your server (Conversions API), Meta receives every order twice. The only thing stopping it from counting both is a shared identifier.
Meta deduplicates when event_name and event_id match across the two events. That's the whole rule. Most broken setups I audit break it in one of two ways.
Failure 1: each side invents its own ID
This is the classic. The theme or tag manager generates something for the browser event, and the server integration generates something else:
// Browser (Pixel)
fbq('track', 'Purchase', { value: 49.0, currency: 'GBP' }, { eventID: crypto.randomUUID() });
// Server (Conversions API)
{ "event_name": "Purchase", "event_id": "1047", "action_source": "website" }
Two different IDs, so Meta has no way to know they're the same order. It counts both, and your reported revenue doubles.
Failure 2: the ID isn't unique
The quieter bug. If the "ID" is a checkout ID that gets reused, a session ID, or a timestamp, two genuinely different orders can share it. Meta then correctly merges them into one, and real sales disappear from reporting with no error anywhere.
Double-counting looks like good news, so someone investigates. Silent loss looks like a slow decline and gets blamed on the ads.
The fix: derive it from the order, once
Use something that is unique per order and that both sides can read: the order ID.
// Browser: on the order confirmation page
const eventId = `order_${order.id}`;
fbq('track', 'Purchase', { value: order.total, currency: order.currency }, { eventID: eventId });
// Server: when the order is created
const payload = {
data: [{
event_name: 'Purchase',
event_time: Math.floor(Date.now() / 1000),
event_id: `order_${order.id}`, // same string as the browser
action_source: 'website',
user_data: { em: [sha256(order.email.trim().toLowerCase())] },
custom_data: { value: order.total, currency: order.currency }
}]
};
If you must use a random UUID, generate it once and pass it to the other side (for example, store it on the order). Two independently generated UUIDs are a duplication machine.
Also watch for prefixes: order_1047 on one side and 1047 on the other is a mismatch, not a detail.
Hash customer data before it leaves your server
em, ph and the other user fields must be SHA-256 hashed (after normalising, e.g. trimming and lowercasing email). Check a real payload before going live. A bug here isn't a tracking bug, it's sending raw personal data to a third party.
Prove it works
- Place one real order. Note the order number.
- In DevTools, filter the Network tab for
facebookand read theeventIDon the Purchase request. - Find the server event for the same order in your logs or Events Manager and read its
event_id. - Compare them character for character.
- Reconcile a settled week of orders against Events Manager purchases. Well above your order count means duplication; well below means loss.
I wrote a longer version for store owners, including a table of which identifiers are safe to use, here: Meta CAPI duplicate purchase events: how deduplication actually works.
I'm Kamran Arshad. I run Google Ads, Meta Ads and SEO with conversion tracking behind every campaign at Marketing Kamran.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.