The Meta Conversions API (CAPI) sends conversions to Meta from your server instead of the browser, so the events ad blockers and browser privacy controls strip from the client-side Pixel still reach Meta. On a Next.js app the clean setup is to keep the Pixel firing in the browser, send the same event from a Route Handler, and tie the two together with a shared event_id so Meta deduplicates them into one. Done right, you recover lost conversions without double-counting.
Why server-side at all
The browser Pixel is fragile. Around 30% of internet users run ad blockers at least some of the time (GWI via Backlinko), and those tools — along with Safari's tracking prevention and consent tooling — routinely block the Pixel before it fires. The Conversions API runs on your server, where none of that applies, so it captures conversions the Pixel misses. The recommended setup is not Pixel or CAPI; it's both, sending the same events through two paths for redundancy.
How deduplication actually works
This is the part teams get wrong. When the Pixel and CAPI both report the same purchase, Meta keeps one — but deduplication only checks event_name and event_id (Meta for Developers). It does not use email, phone, fbp, or fbc to dedupe; those identifiers are for matching the event to a Facebook profile, a separate job.
So the rule is simple: generate one unique event_id per user action and pass the identical string to both the Pixel and the Conversions API. A UUID v4 works well. Any mismatch — even different casing or a stray space — breaks deduplication and the conversion gets counted twice.
Step 1: fire the Pixel with an event ID
In the browser, generate the ID and pass it to the Pixel as eventID:
const eventId = crypto.randomUUID();
fbq("track", "Purchase", { value: 49.0, currency: "USD" }, { eventID: eventId });
Keep that eventId so your server can send the same value — for example by posting it to your own endpoint alongside the order.
Step 2: send the same event from a Route Handler
Create a server route (e.g. app/api/meta/route.ts) that calls Meta's Graph API with the matching event_id:
await fetch(`https://graph.facebook.com/v21.0/${PIXEL_ID}/events?access_token=${TOKEN}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
data: [{
event_name: "Purchase",
event_time: Math.floor(Date.now() / 1000),
event_id: eventId, // identical to the Pixel's eventID
action_source: "website",
user_data: { /* see step 3 */ },
custom_data: { value: 49.0, currency: "USD" },
}],
}),
});
Run this server-side only — the access token is a secret and must never reach the browser.
Step 3: get user_data right (hashing rules)
The user_data object is what lets Meta match the event to a user, and it has strict, easy-to-miss rules (Meta for Developers):
- Hash all PII with SHA-256 before sending — email, phone, name. Send these only with real consent.
- Do not hash
fbpandfbc. Hashing them breaks matching entirely.fbpcomes from the_fbpfirst-party cookie;fbcis derived from thefbclidquery parameter when a user arrives from a Meta ad. - Only set
fbcwhen a realfbclidexists in the landing URL. Never fabricate one — invented values degrade match quality. - Include
client_ip_addressandclient_user_agentfrom the incoming request when you can.
On Next.js, read _fbp/fbc from cookies on the server (or pass them from the client with the order), and pull IP and user-agent from the request headers in the Route Handler.
Step 4: verify in Events Manager
Open Events Manager → your Pixel → Test Events, complete a purchase, and click into the event. You should see it arriving from both the browser and the server, and a deduplication status confirming the two were merged. If you see the same purchase twice, your event_id values don't match — fix that before going live.
Common mistakes that break it
Most CAPI problems come from a short list of avoidable errors:
- Mismatched
event_id. A difference in casing or whitespace between the Pixel'seventIDand the server'sevent_idproduces two events instead of one. Generate the ID once and pass the exact string to both. - Hashing
fbporfbc. These are not PII and must be sent raw; hashing them silently destroys matching. - Fabricating
fbc. Only attach it when a realfbclidis in the URL. A made-up value degrades match quality rather than improving it. - Leaking the access token. The token is a server secret. Keep it in an environment variable read only inside the Route Handler — never expose it to the browser bundle.
- Sending PII without consent. Email and phone go through SHA-256, but you should only collect and transmit them where the user has actually consented.
Key things to remember
CAPI is a redundancy layer, not a replacement for the Pixel — and it still won't make Meta and GA4 agree, because the two platforms count conversions over different windows regardless of how the data arrives. Treat owned, first-party events as your source of truth, use CAPI to make Meta's own optimization signal more complete, and lean on the event_id discipline so completeness never turns into double-counting.