Meta CAPI: sending server-side conversions without double counting
Meta’s Conversions API recovers the purchases your browser pixel loses. This guide covers deduplication, the parameters that actually move match quality, and the mistakes that teach your campaign the wrong thing.

Meta’s Conversions API — CAPI — is the path where an event goes from your server straight to Meta, without passing through the person’s browser. It exists because the browser pixel loses events: ad blockers, tabs closed before the pixel fires, iOS tracking restrictions and plain network failures.
The gain is real, but it depends on three decisions almost everyone gets wrong on the first implementation.
1. Deduplication: the mistake that doubles your revenue
If you keep the browser pixel and switch on CAPI, every purchase is reported twice. Meta knows how to resolve that — as long as both sends carry the same event identifier.
// Browser
fbq('track', 'Purchase', { value: 1997, currency: 'USD' },
{ eventID: 'order-88213' })
// Server (CAPI) — SAME event_id
{
"event_name": "Purchase",
"event_id": "order-88213",
"event_time": 1755340000,
"action_source": "website",
"user_data": { "em": ["<sha256>"], "ph": ["<sha256>"] },
"custom_data": { "value": 1997, "currency": "USD" }
}Without a matching event_id on both sides you do not have double coverage — you have double counting. The report looks great and the campaign optimizes against a number that does not exist.
The identifier has to be stable and derived from the business — an order number, a transaction id — never a random value generated at send time, which by definition would differ between the two paths.
2. The parameters that move match quality
Meta can only attribute the conversion if it can recognize the person. The user_data parameters do not carry equal weight, and it pays to know where to spend effort:
| Parameter | What it is | Practical weight |
|---|---|---|
| em | Email, sha256 hashed | High — the most decisive one |
| ph | Phone, sha256 hashed, with country code | High |
| fbc | Ad click cookie | High when present: it is direct attribution |
| fbp | Browser pixel cookie | Medium, helps stitch sessions |
| external_id | Your own identifier for the person | Medium, improves consistency |
| ct, st, zp, country | Location | Low alone, useful in combination |
| fn, ln, ge, db | Name, gender, birth date | Low, but additive |
The common mistake here is sending only what arrived in the event payload. The email usually exists in your database even when it was not in the request — which is why user_data should be assembled from the identity graph, not from the raw payload.
The fbc detail almost nobody fixes
The _fbc cookie is created by Meta’s script when someone arrives with an fbclid parameter in the URL. If that script is blocked the cookie is never born and direct attribution is lost — precisely for the visitor who is hardest to recover later. The fix is to build the cookie in first-party context from the URL’s fbclid, in the format Meta expects.
3. Filtering what goes up
This is the most expensive mistake and the quietest one. If you send every purchase event without checking status, a payment that was generated and never completed becomes a conversion. The campaign then learns to find people who start payments and never finish — and it is very good at doing what it is told.
- Send only approved or completed status; a printed invoice or an expired payment is not a sale.
- Separate first purchases from subscription renewals, or every monthly charge becomes a new acquisition.
- Watch for the same order arriving through two paths (webhook and history sync) — it needs deduplication by business identity.
- Do not send old sales during reprocessing: Meta rejects events older than seven days, and whatever slips through distorts the window.
How to know it is working
Meta’s Events Manager shows match quality, but with delay and in aggregate. The more useful signal is measured on your side: across the payloads you actually delivered, what percentage carried email, phone, fbc and external_id. If email is present in 30% of your sends, no configuration on Meta’s side will rescue matching — the problem is capture.
Parameter coverage is an engineering metric, not a media metric. It gets fixed at collection, and it is measurable before any campaign runs.
In CrazyLeads these three points are the default behavior: the event_id comes from business identity, user_data is assembled from the identity graph, and the status filter is configured against your source’s real payload and tested on recent events before publishing.