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.

