Pixel + CAPI: how to avoid duplicate events
Running Pixel + Conversion API together is Meta's official recommendation in 2026. But if you don't use a shared event_id, you'll count everything twice. This guide explains how to configure deduplication correctly in 8 minutes.
Generate a single event_id per event (UUID v4 or hash of order_id) and send that same ID through both the Pixel (eventID parameter) and the Conversion API (event_id field). Meta deduplicates automatically within a 7-day window. If the IDs are different, it counts as 2 conversions — ROAS inflated by 2×.
The double counting problem
In 2026, Meta recommends running Pixel + CAPI together. The reason: each path covers what the other misses. Pixel catches fast events in the browser; CAPI ensures critical conversions always arrive (even with iOS ATT, AdBlock, etc.).
But there's an obvious problem: if both send the same event (e.g., Purchase), Meta might count it twice. Practical result:
- Ads Manager shows 200 conversions when there were really 100;
- Reported ROAS at 8× when reality is 4×;
- Meta's optimization AI gets confused — it thinks it's nailing the audience, when it's really over-optimizing for the wrong pattern;
- The CFO sees the report, sees the CRM, spots the gap and starts questioning the whole ads operation.
How dedupe works in Meta
Meta automatically deduplicates events that meet three conditions simultaneously:
- Same
event_name— e.g., both as "Purchase"; - Same
event_id— identical string between pixel and CAPI; - Received within 7 days of each other — deduplication window.
When the 3 conditions match, Meta keeps only the first event that arrived and discards the second. Result: 1 conversion counted, not 2.
Generating event_id correctly
The event_id must be:
- Unique per event (don't reuse across different Purchases);
- Stable between pixel and CAPI (generate ONCE, propagate to both);
- Deterministic if possible (same input = same ID, easier to debug).
Three common approaches:
Option 1: UUID v4 (most common)
// JavaScript
const eventId = crypto.randomUUID();
// → 'e4eaaaf2-d142-11ec-9d64-0242ac120002'
Pros: zero collision guaranteed. Cons: random, not tied to the order.
Option 2: Hash of order_id (recommended for e-com)
// Pseudocode
const eventId = 'purchase_' + sha256(orderId + timestamp).slice(0, 16);
// → 'purchase_a3f2b9c8d4e5f6a1'
Pros: traceable (you can find it in logs by order_id). Cons: requires that the timestamp matches between pixel and CAPI.
Option 3: order_id directly (simple)
const eventId = String(order.id); // '#10042'
Pros: dead simple. Cons: if an order has multiple events (Purchase, AddPaymentInfo), you need a suffix to differentiate (10042_purchase, 10042_payment).
Implementation in the client-side pixel
// 1. Generate event_id on the server (render into HTML)
// 2. Pass it to the pixel via the eventID parameter
fbq('track', 'Purchase', {
value: 99.90,
currency: 'USD',
content_ids: ['SKU-001'],
content_type: 'product'
}, {
eventID: '' // ← this is the shared ID
});
Watch out: the eventID parameter (camelCase) goes in the third argument of fbq('track', ...), not the second. Common mistake.
Implementation in the server-side CAPI
// PHP / Node / Python — payload for Meta CAPI
const payload = {
data: [{
event_name: 'Purchase',
event_time: Math.floor(Date.now() / 1000),
event_id: 'purchase_a3f2b9c8d4e5f6a1', // ← same ID as the pixel
action_source: 'website',
user_data: {
em: [sha256('[email protected]')],
ph: [sha256('+15551234567')],
fbp: req.cookies._fbp,
fbc: req.cookies._fbc
},
custom_data: {
value: 99.90,
currency: 'USD'
}
}],
test_event_code: 'TEST12345' // remove in production
};
await fetch(`https://graph.facebook.com/v19.0/${pixelId}/events`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...payload, access_token: META_ACCESS_TOKEN })
});
The 7-day deduplication window
Meta keeps the event_id in cache for 7 days. If pixel sends today and CAPI sends 8 days later with the same ID, it counts as 2 events.
In special cases (e.g., payment gateway webhook that may take up to 10 min to confirm sale), that's fine. But never send the pixel now and CAPI 3 days later — it will duplicate.
Does it work for TikTok and Google too?
TikTok Events API
Same logic. Use event_id in the TikTok Events API payload identical to the TikTok Pixel event_id:
ttq.track('CompletePayment', {
value: 99.90,
currency: 'USD',
event_id: 'purchase_a3f2b9c8d4e5f6a1' // ← shared with Events API
});
Google Ads Enhanced Conversions
Google deduplicates automatically via match key (hashed email + hashed phone). No explicit event_id needed — but you must ensure the client-side and server-side pixel send exactly the same hashed email.
How to validate that dedupe is working
- Open Meta Events Manager → Diagnostics;
- Look for the warning "Server and browser events received without event_id" — if you see it, you have non-deduplicated events;
- Check the "Deduplication rate" metric per event — should be between 0.85 and 1.0. Below 0.5 = not deduplicating properly;
- Compare Ads Manager vs CRM counts — should match at 85%+ (it was 50-60% before CAPI).
FAQ
What happens if pixel and CAPI have different event_id?
Meta counts them as two separate events. Your Ads Manager reports 2× the real number of conversions, inflating ROAS upward. This confuses Meta's AI (it optimizes for the wrong number) and distorts reports for the CFO/client.
How do you generate event_id correctly?
Use a unique, stable identifier per event — typically UUID v4 or a SHA-256 hash of timestamp + order data. The same event_id must be passed through the pixel (eventID parameter) AND through CAPI (event_id field). Do not generate two separate IDs.
What is Meta's deduplication window?
Meta deduplicates events with the same event_id, event_name and fbp/fbc within 7 days. If the pixel sends today and CAPI sends 8 days later, it counts as two events.
Do TikTok and Google also use event_id?
Yes. TikTok deduplicates via identical event_id in pixel + Events API. Google Ads enhanced conversions deduplicate automatically via match key (hashed email/phone). The logic is the same — generate once, propagate to both paths.
Can you use the order ID as event_id?
Yes, as long as it is unique. Order ID is a good source because it already exists in your database. Be careful: if the customer refunds and re-orders, keep the original ID (Meta will treat them as two different events only if the ID changes).
Trakvo deduplicates automatically
Configure pixel + CAPI on Trakvo and the system generates a shared event_id, syncs between both paths and monitors deduplication rate in real time.
Talk to the team