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.
Top comments (0)