Meta Pixel + CAPI: the no-nonsense guide to checkout tracking in 2026
How to set up Meta Pixel and Conversions API together so your Purchase events actually fire — even with ad blockers, iOS 14, and Safari ITP.
If you run Meta ads to drive checkout, your single most expensive piece of infrastructure is the Purchase event. Get it wrong and you'll waste 20–40% of your ad spend optimizing against partial data. Get it right and Meta's algorithm learns who buys, scales the audience that does, and quietly halves your CAC.
This post is a practical guide. We'll cover how Meta Pixel and the Conversions API (CAPI) actually work together, why client-side tracking alone is no longer enough, and the specific architecture that survives iOS 14 / Safari ITP / ad blockers in 2026.
A short timeline of how tracking broke
Three things happened over five years that broke the simple "drop a pixel" world.
2020: iOS 14.5 ATT. Apple required apps (including Meta's) to ask users for permission before tracking across other companies. Most users said no. Meta lost the ability to match in-app behavior with off-site purchases for a huge chunk of iPhone buyers.
2021–2022: Browser ITP escalations. Safari's Intelligent Tracking Prevention and Firefox's equivalent started stripping third-party cookies and capping first-party cookie lifetimes. Pixel events that depended on cross-site identity stopped reliably matching.
2023+: Ad blockers everywhere. A meaningful share of buyers (often 20–30% in technical audiences) run uBlock Origin or similar. The pixel script itself never loads.
The cumulative effect: a typical e-commerce shop running pixel-only sees 30–45% of real Purchase events go unreported. Meta's optimizer sees fuzzy data, scales the wrong audience, and CAC climbs.
The fix is server-side reporting via CAPI, deduplicated against the client-side pixel.
How Pixel and CAPI work together
A modern setup sends each event twice: once from the browser (Pixel) and once from your server (CAPI). Meta deduplicates them using a shared event_id and uses whichever arrives — but it almost always has the server one, even when the browser one is blocked.
Browser → fbq('track', 'Purchase', {...}, {eventID: 'pt_8Hq3LKz'})
│
▼
Server → POST graph.facebook.com/v18.0/{pixel_id}/events
{ event_id: 'pt_8Hq3LKz', event_name: 'Purchase', ... }
The same event_id on both ensures Meta counts the purchase once. The server-side hit always works (no ad blocker, no cookie loss, no Safari ITP).
This isn't optional in 2026. The honest version: pixel-only is data-broken; CAPI-only loses some attribution detail; pixel + CAPI deduplicated is the only setup that gives Meta a clean signal.
The minimum viable architecture
Here's the smallest possible setup that actually works. Five pieces:
- An
event_idgenerated server-side at the moment the purchase completes. Must match between Pixel and CAPI. - A
user_datablock with hashed buyer info (em,ph,fn,ln,ct,st,zp,country). - A
custom_datablock with the transaction details (value,currency,content_ids,content_type). - An
event_source_url— the page the buyer was on when they purchased. - A backend HTTP call to Meta's
/eventsendpoint, with your access token and pixel ID.
The hashing matters. Meta won't accept raw email or phone. You hash with SHA-256, lowercase trimmed, hex-encoded.
import { createHash } from "node:crypto";
function hash(value: string) {
return createHash("sha256").update(value.toLowerCase().trim()).digest("hex");
}
const userData = {
em: hash(buyer.email),
ph: buyer.phone ? hash(buyer.phone) : undefined,
fn: buyer.firstName ? hash(buyer.firstName) : undefined,
ln: buyer.lastName ? hash(buyer.lastName) : undefined,
ct: buyer.city ? hash(buyer.city.toLowerCase()) : undefined,
country: buyer.country ? hash(buyer.country.toLowerCase()) : undefined,
client_ip_address: req.headers["x-forwarded-for"],
client_user_agent: req.headers["user-agent"],
fbc: cookies.get("_fbc"),
fbp: cookies.get("_fbp"),
};
The fbc and fbp cookies are the click-id and browser-id Meta sets on the buyer's session. They're the bridge that lets Meta attribute a purchase to a specific ad click. Always include them when you have them.
The CAPI request
The shape of the POST request:
await fetch(`https://graph.facebook.com/v18.0/${PIXEL_ID}/events`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
data: [
{
event_name: "Purchase",
event_time: Math.floor(Date.now() / 1000),
event_id: eventId,
event_source_url: "https://example.com/checkout/success",
action_source: "website",
user_data: userData,
custom_data: {
currency: "USD",
value: 29.0,
content_ids: ["sku-001"],
content_type: "product",
order_id: order.id,
},
},
],
access_token: process.env.META_CAPI_ACCESS_TOKEN,
}),
});
Meta's API returns { events_received: 1, fbtrace_id: '...' } on success. Log the fbtrace_id — when something is wrong, this is what Meta support will ask for.
The corresponding Pixel call
On the success page, fire the matching client-side event:
<script>
fbq(
"track",
"Purchase",
{
value: 29.0,
currency: "USD",
content_ids: ["sku-001"],
content_type: "product",
},
{
eventID: "pt_8Hq3LKz", // same ID server sent
},
);
</script>
If the buyer has an ad blocker, this script never runs — and that's fine, because the server already fired the event with the same ID.
Where it goes wrong
After helping many creators wire this up, here's a list of the bugs that come up over and over.
1. Mismatched event_id. Both Pixel and CAPI need the exact same string. If you generate one in the browser and a different one on the server, Meta double-counts. Solution: generate the ID server-side at purchase time, return it in the success response, use it for both calls.
2. Forgetting to hash. Meta returns a 400 error mentioning "invalid hash" if you send raw email. Hash every PII field with SHA-256 lowercase.
3. Missing fbc/fbp cookies. Without these, the purchase isn't tied to the ad click. Read the cookies on the success page request — if the buyer came via a Meta ad, _fbc will contain the click ID and _fbp the browser ID.
4. Server-side request timing out. Meta's /events endpoint can take 200–500ms. Make the call async — don't block your success page render on it. We use a fire-and-forget pattern with Promise.allSettled and 2-second timeout.
5. Test events left enabled. The CAPI API supports a test_event_code parameter for verification. People leave it set in production. Result: Meta routes the events to "test mode" instead of real attribution. Strip the code in prod.
6. Sending values in cents, not dollars. Meta wants 29.00 for $29, not 2900. The opposite of how Stripe works. Easy mistake to make if you copy from your payment record.
7. Localhost URLs in event_source_url. Meta silently drops events with non-public URLs. If you're testing locally, use a tunnel (ngrok, cloudflared).
How to verify it's working
Meta gives you a few real verification tools.
Test Events tab in Events Manager. Set a test_event_code and watch events appear in real time. This confirms your CAPI calls are reaching Meta and parsing correctly.
Event Match Quality. In Events Manager, each event source gets a score from 0–10 measuring how completely Meta can match buyers to ad clicks. Above 7 is good. Below 5 is broken — you're missing fbc, fbp, or hashed PII.
Deduplication rate. Under "Diagnostics," Meta shows you what percent of Pixel + CAPI events deduped successfully. Should be 95%+. Lower means your event_ids are mismatched.
Aggregated Event Measurement priority. iOS 14.5+ caps you at 8 prioritized events per domain. Purchase should be priority 1 (or 2 if you have AddToCart higher). Set this in Events Manager.
What good looks like
Once your setup is right, you should see roughly:
- Event Match Quality: 7.5+ on Purchase
- Deduplication rate: 95%+
- Meta Reported Purchases vs. actual orders: within 3–5%
- Conversion lift after rollout: 15–35% improvement in Meta-reported conversions, which translates to lower CAC
The lift number is real and it's the reason this matters. A creator running $5k/month in Meta ads who fixes CAPI typically recovers $750–$1,750/month in previously-unreported purchases. That data flows back into Meta's optimizer, which then targets more accurately, which lowers CPMs further. It compounds.
When to use a platform that handles this for you
Wiring all of this from scratch is two to four days of careful work plus an ongoing maintenance burden. Most creators will end up using a tool that handles it. Purpleturret ships pixel + CAPI deduplicated out of the box — you paste your pixel ID and CAPI token, and Purchase events flow with hashed PII and proper fbc/fbp propagation. Most of the "where it goes wrong" list above doesn't apply.
If you're DIY-ing on Stripe direct or a custom checkout, this guide is the path. If you'd rather not, that's a real reason to consider a platform that's already solved it.
A final note on Google and TikTok
The same architecture applies to:
- Google Ads server-side conversions — Google offers an "Enhanced Conversions" CAPI that works almost identically.
- TikTok Events API — TikTok's server-side equivalent. Same pattern:
event_id, hashed PII, dedup with the client-side pixel.
If you run multi-channel ads, build the server-side dispatch once and fan out. The cost is a single function with three providers; the benefit is consistent, ad-blocker-proof tracking across your entire paid stack.
Purpleturret handles pixel + CAPI deduplicated by default. No code, no edge functions, no missing events. Start free.