Server-side conversions with Meta CAPI: what breaks and how to fix it
Meta cut off third-party cookie tracking, so everyone rushed to implement Conversions API. Then ROAS numbers got weirder, not better. If you've deployed CAPI and your reported revenue still doesn't match Shopify, you're not alone — most implementations have at least one silent failure point that nobody catches until finance asks why the numbers don't reconcile.
Deduplication fails more than it works
CAPI is supposed to work alongside the browser pixel, not replace it. Meta deduplicates events using event_id and event_name matching between the two sources. When that matching breaks, you get double-counted purchases inflating your conversion volume, or Meta discarding legitimate server events because it thinks they're duplicates.
- The event_id generated client-side never makes it to your server-side call because your checkout flow and your backend don't share state.
- Event names don't match exactly — "Purchase" on the pixel and "purchase" on the server, or a custom event name that drifts between the two payloads.
- Timestamps are so far apart that Meta's matching window doesn't catch them as the same event.
Fix this by generating the event_id once, at the earliest point in the transaction, and passing that same string through to both the browser pixel call and the server-side API call. Don't regenerate it on the backend. Log it on both ends during QA and diff the logs — if the IDs don't match 1:1, your dedup is broken and every dashboard number downstream is wrong.
Match quality quietly degrades
CAPI's entire value proposition is that it can match a server-side event to a Meta user even without third-party cookies. That matching depends on hashed customer data — email, phone, external ID — passed correctly in the user_data object. This is where most implementations bleed accuracy without anyone noticing, because Meta doesn't reject bad data outright. It just matches less of it.
- Emails aren't lowercased and trimmed before hashing, so "John@Email.com " and "john@email.com" hash to different values and Meta treats them as different people.
- Phone numbers get sent without E.164 formatting, so country codes are missing or inconsistent.
- External IDs (your internal customer ID) aren't sent at all, which means you're relying entirely on PII matching instead of a persistent identifier that survives across sessions and devices.
Normalize every field before hashing — lowercase, trim whitespace, strip formatting on phone numbers — and always include external_id if you have a logged-in customer or a durable cookie-based ID. Check your Events Manager match quality score weekly, not once at launch. It degrades as new checkout flows, new plugins, or new agency hands touch the tracking code.
Server events arrive with stale or missing data
A lot of CAPI implementations pull event data from a webhook — Shopify's order creation webhook, for example — and fire it straight to Meta. The problem: webhooks fire on order creation, not on the actual customer action, and the payload often lacks the browser-side context Meta wants for attribution, like fbc (click ID) and fbp (browser ID) cookies.
- Without fbc and fbp passed through, Meta can't tie the server event back to the ad click that drove it, so it falls back to weaker matching or attributes nothing at all.
- Webhook delays mean the event might hit Meta minutes or hours after the browser-side event, which can break deduplication windows and confuse attribution timing.
- Refunds and cancellations processed through the webhook pipeline sometimes get sent as new purchase events instead of being excluded or reversed, inflating revenue.
Capture fbc and fbp client-side at the moment of purchase and store them with the order — in an order note, a custom field, whatever your platform allows — so your server-side call has them available when the webhook fires. And explicitly filter your webhook logic to exclude refunds, test orders, and duplicate order-update events, not just order-creation events.
Consent signals don't travel with the event
If you're running in markets with GDPR, CCPA, or similar frameworks, your consent management platform blocks the pixel when a user opts out. But server-side calls often bypass that same consent check because they're not running in the browser — the backend doesn't know what the user chose. This creates a compliance gap and, ironically, can also create bad data: events firing for users who explicitly opted out get sent anyway, muddying your true opted-in audience size and putting you at legal risk.
Pass consent state through to your backend as part of the event payload — store it with the session or order — and gate your server-side send logic on that same flag. Meta's Limited Data Use flag exists for exactly this reason; use it, and make sure your consent platform's opt-out status is actually reaching your server logic, not just your client-side pixel snippet.
Nobody validates after launch
The most common failure isn't technical — it's organizational. Teams stand up CAPI, check the Events Manager diagnostics tab once to confirm it's "connected," and never look again. Meanwhile a Shopify app update changes the checkout flow, a new landing page skips the tracking snippet, or an agency swaps ad platforms and forgets to update the event mapping.
- Set a recurring check — monthly, minimum — comparing Meta-reported purchases and revenue against your actual order data from your warehouse or Shopify admin.
- Watch the deduplication rate in Events Manager, not just the match quality score. A sudden drop means something upstream broke.
- Treat any checkout or theme update as a tracking regression risk, and test the full purchase event flow after every deploy that touches the buy flow.
CAPI isn't a set-and-forget fix for post-iOS14 tracking loss. It's a pipeline with the same failure modes as any data pipeline: bad joins, stale data, missing fields, and no monitoring. Treat it that way — validate it like you'd validate any revenue-critical data feed — and the ROAS numbers you report will actually mean something.