For business6 min read

Meta Conversions API: why the Pixel alone is no longer enough and how to set it up without duplicates

Ad blockers swallow some of your Pixel events. How the Conversions API sends purchases from your server, how to avoid duplicates, and what to do with emails, phones and consent.

Meta Pixel runs in the visitor's browser, so it depends on that browser. If an ad blocker is on, the Pixel file never loads and the purchase never reaches Meta — even though the money is already in your account. The Conversions API (CAPI from here on) sends the same event from your server, where there are no ad blockers.

This article covers when CAPI is worth setting up, how to pair it with the Pixel so purchases are not doubled, and how to handle personal data. We write from practice: we recently set up CAPI for a WordPress website with a booking plugin and card payments.

Why the Pixel alone is no longer enough

The first sign usually shows up when you compare reports: a purchase is in Google Analytics but missing in Meta. One of the causes is a blocked Pixel file, fbevents.js. Meta never sees that purchase, cannot credit it to an ad and does not learn from it.

After launching CAPI, we saw exactly these cases in our log: the Pixel was blocked in the browser, and the server sent the purchase anyway. Without the server side, Meta would never have received those purchases.

Two caveats up front:

  • CAPI does not bypass cookie consent. If the visitor declined marketing cookies, the server must send nothing — exactly like the Pixel.
  • CAPI does not fix browser-side mistakes. If the Pixel sends duplicates or fires Purchase before payment, the server will simply repeat those mistakes. Fix the Pixel first, then add the server.

How it works

  1. The visitor pays and the payment provider confirms the payment.
  2. In the browser, the Pixel sends Purchase with a unique event ID (event_id).
  3. Your server sends the same Purchase with the same event_id to the Meta Graph API endpoint /<pixel_id>/events.
  4. Meta merges both messages into one purchase. If the Pixel was blocked, the server message remains.

The server request looks roughly like this (shortened; the access key is passed as the access_token parameter):

POST https://graph.facebook.com/<version>/<pixel_id>/events
{
  "data": [{
    "event_name": "Purchase",
    "event_time": 1760000000,
    "event_id": "purchase-a1b2c3",
    "action_source": "website",
    "event_source_url": "https://example.com/thank-you",
    "user_data": {
      "em": ["<email SHA-256>"],
      "ph": ["<phone SHA-256>"],
      "client_ip_address": "…",
      "client_user_agent": "…"
    },
    "custom_data": { "value": 45.00, "currency": "EUR" }
  }]
}

No duplicates as long as event_id matches

When an event arrives from both the browser and the server, Meta has to know it is the same purchase. For that, both messages need the same event_name and the same event_id. In the Pixel code it goes in as the fourth argument:

fbq('track', 'Purchase', { value: 45.00, currency: 'EUR' }, { eventID: 'purchase-a1b2c3' });

Note that the Pixel parameter is called eventID, while the server request uses event_id. They are easy to mix up.

The best way to build event_id is from the order itself — for example, your internal order number plus the event name. That way the browser and the server both know it without talking to each other. A random number generated only in the browser is unknown to the server, and deduplication will not work. If event_id does not match or is missing, one purchase can be counted twice, and you are back to the problem you were trying to solve.

Personal data: hash before sending

To match the event to a person, the server also sends the email address and phone number. Not in plain text, though: both are hashed with SHA-256 before they are sent. The data has to be normalized before hashing, otherwise it will not match what Meta has:

  • email — lowercase, no spaces at the start or end;
  • phone — international format, digits only, no "+" and no spaces: the Lithuanian 8 6xx xxxxx becomes 3706xxxxxxx.

We do not store unhashed data anywhere — not in the log, not in extra tables. The log only keeps whether the event was sent and what Meta replied. Meta accepts the IP address and the browser identifier (user agent) unhashed.

Because the data travels from your server to Meta, your privacy policy has to say so. Update it before launch, not after.

Consent applies to the server too

In the browser, the Pixel waits for consent on its own — the cookie plugin holds it back. The server does not know that, so it has to check consent itself: read the consent plugin's cookie (Complianz, for example) and send the event only if the visitor accepted marketing cookies. Without consent the server sends nothing, not even hashed data.

The access key and the test code

The CAPI access key is generated in Events Manager settings. It belongs in the server configuration — on WordPress, for example, as a constant in wp-config.php — not in the database and not in email threads. If the key travelled by email, generate a new one after launch.

While testing, the Events Manager "Test events" tab gives you a test_event_code. As long as it is attached to server requests, events go through as test events and appear in the "Test events" tab. Meta says to use this field for testing only, so it must be cleared once you are done. The easiest way is to keep it as a separate setting: fill it in to test, clear it to go live. A forgotten test code is an easy mistake to make and a hard one to notice.

Meta replies to every request — the response includes events_received, the number of events accepted. Log any error response and alert the responsible person straight away, instead of letting errors sit there silently.

Why the numbers still will not match

Even with CAPI, Meta, GA4 and your own system will show different numbers. The main reason is attribution windows. By default, Meta credits a purchase to an ad if it happened within 7 days of a click or 1 day of a view. GA4 can look back up to 90 days. That is normal: compare trends, not exact figures, and treat the paid orders in your own system as the only truth.

What it costs

Some platforms and plugins offer a CAPI integration through Meta partners — check yours first. Booking plugins and custom systems may not have one, or it may send the wrong events; then a custom workflow is built. In our pricing, integrations like this fall in the €500–€2,000 range, and we give an exact price once we have seen your system. More on our automation service page.

If you are hiring another developer, put at least these in the brief: the event_id format, data normalization and hashing, the server-side consent check, where the access key lives, and an error log. We covered how to write such a brief in How to write a software brief.

What to do tomorrow

  1. Compare last month's paid orders: how many are in your system, how many in GA4 and how many in Meta.
  2. Check whether your platform or plugin already has a Conversions API integration.
  3. Before building anything, decide what event_id will be made of and how the server will check consent.

Frequently asked questions

Does the Conversions API bypass cookie consent?

No. The server has to check the same consent as the Pixel. Without consent to marketing cookies, it sends nothing.

Can I use only the Conversions API, without the Pixel?

You can, but Meta recommends using both. The Pixel sets the _fbp cookie, which the server can pass along with the event, making it easier for Meta to match the event to a person. The server, in turn, protects you from ad blockers.

How do I know whether the data is matched well?

Events Manager shows a match quality score (Event Match Quality) for server events. If it is low, first check that email and phone are being sent and that they are normalized before hashing.

More posts

All posts