Skip to main content

What separates a production-grade WooCommerce Conversion API implementation from a toy one

· 14 min read
Aleksandar Vucenovic
Chief Growth Officer

On the surface, every WooCommerce plugin that talks about the Conversions API does the same thing: take an order, hash an email, POST a payload to an ad platform. In practice, the gap between "POSTs a payload" and "actually delivers reliable, deduplicated, attributable conversions across every payment gateway and edge case a real store throws at it" is enormous. This post is about that gap.

After processing many millions of events across hundreds of WooCommerce stores, we have catalogued dozens of edge cases that simply do not surface on a single test site. Each one is small. Together they decide whether a store's reporting is honest or fiction.

TL;DR

A production-grade WooCommerce Conversions API implementation differs from a basic one on at least seven dimensions:

  1. Single abstract S2S base class for every ad platform (Facebook, TikTok, Pinterest, Snapchat, Reddit, GA4 MP), so every fix applies everywhere.
  2. Three-tier identifier resolution (live session → order meta → WooCommerce-stored fallbacks) plus a server-side User-Agent detector for Stripe, PayPal, Klarna, Mollie, Adyen, Square, and wp-cron callbacks.
  3. Per-platform purchase idempotency keys (one meta flag per ad platform), not a single global flag that locks out every other platform after the first success.
  4. E.164 phone normalisation with per-platform hashing rules.
  5. Edge-case coverage: iframe checkouts, manual back end orders, pay-for-order links (priority 5 hook), the full WooCommerce Subscriptions lifecycle (Subscribe / RecurringSubscriptionPayment / CancelSubscription), and GA4 partial/full refunds.
  6. No synthetic _fbp inflation. Sending a server-generated random value to inflate Meta's Event Match Quality dashboard does not produce real attribution, audience matching, or optimizer lift, and creates collision risk when the real pixel later sets a genuine cookie.
  7. Optional edge offload via a Cloudflare Worker on a first-party subdomain to remove the per-order outbound HTTP load from the WooCommerce server.

The rest of this post explains each one in detail.

A single architecture for every platform

The Pixel Manager implements the same server-to-server (S2S) interface for every supported ad platform: Facebook, TikTok, Pinterest, Snapchat, Reddit, and Google Analytics 4 Measurement Protocol. Every implementation extends a single abstract base class. The base class owns identifier collection, payment-gateway detection, idempotency, retry-safe status hooks, and payload assembly. The platform-specific subclasses only add what is genuinely platform-specific: the API endpoint, the auth header shape, and the event-name mapping.

This is not an aesthetic preference. It is the only way to make sure that a fix discovered in the Facebook integration (for example, "Klarna fires the order-paid hook from a server-side cron that has no User-Agent") is automatically applied to TikTok, Pinterest, Snapchat, and Reddit on the next release. Plugins that ship one bespoke S2S implementation per platform inevitably end up with five different definitions of "what counts as a paid order."

Identifier resolution: the part nobody talks about

The single hardest problem in server-side tracking is reconstructing a real browser identity from a payment that arrived via a server-side webhook. By the time a Klarna, Mollie, Stripe redirect, PayPal IPN, Adyen, Square, or wp-cron callback marks an order as paid, the original customer's browser is long gone. There is no _fbp cookie in the request. There is no User-Agent in the request. There is no client IP that means anything (it is the gateway's IP, not the customer's).

Every server-side tracking event needs four things to land cleanly in an ad platform's match graph:

  1. The browser identifier the platform set on the original visit (_fbp, _ttp, _pin_unauth, _scid, etc.).
  2. The click identifier from the ad URL (fbclid, ttclid, epik, ScCid, rdt_cid, gclid).
  3. The customer's User-Agent string from the browser that placed the order.
  4. The customer's real IP address.

The Pixel Manager solves this with a three-tier resolution chain:

  1. Live session. During checkout, the relevant cookies and click IDs are captured into the WooCommerce session.
  2. Order meta. At order creation, those values are persisted onto the order itself, along with the browser's User-Agent and the customer's IP. They survive the customer closing the tab.
  3. WooCommerce-stored fallbacks. When even the order-meta path is missing values (for example because a back end user marked an order paid manually), the plugin pulls WooCommerce's own stored UA and IP for that order.

On top of that, there is a server-side User-Agent detector for the cases where the order-paid hook fires from a context with no usable UA at all: Stripe webhooks, PayPal IPNs, Klarna server callbacks, Mollie webhooks, Adyen notifications, Square webhooks, and wp-cron runs are all explicitly recognised and handled so the resulting payload is not poisoned with a User-Agent that says "Stripe-Webhook" or "wp-cron".

This sounds like plumbing. It is the difference between Meta's Event Match Quality reading 8.5 and reading 4.5 on the same store.

Per-platform idempotency

Most plugins guard against duplicate purchase events with a single boolean meta key on the order: _purchase_event_fired = 1. This works fine until the store turns on a second platform. Now the same flag has to gate Facebook, TikTok, Pinterest, Snapchat, Reddit, and GA4 simultaneously, and the first platform that succeeds locks out the other five.

The Pixel Manager uses one meta key per platform: _pmw_facebook_purchase_hit, _pmw_tiktok_purchase_hit, _pmw_pinterest_purchase_hit, and so on. Each platform's pipeline can succeed, fail, retry, and recover independently. A Facebook outage does not silently break TikTok reporting.

Phone numbers, properly

Every platform wants the phone number hashed. Some want SHA-256 of the full E.164 string. Some want it without the +. Some accept multiple formats. Some require the country code, others reject it.

The Pixel Manager normalises every phone number to E.164 using libphonenumber (the PHP port of Google's reference library), then applies the per-platform hashing rule. A store ships one phone field; it lands on six platforms in the format each one actually accepts. Stores that try to do this themselves usually pick one format, hash it, and silently destroy match rates on every platform that expects something different.

Email addresses get the same treatment, including platform-specific normalisations like Gmail alias stripping where the platform documents that it removes the +suffix and dots before hashing.

Edge cases the abstract base class fixes for free

Because every platform inherits the same purchase pipeline, the following are handled identically across all of them:

  • Iframe checkouts. Some payment plugins render the WooCommerce thank-you page inside an iframe. A naive browser pixel fires twice (parent and iframe) or zero times (no thank-you page reached). The plugin detects iframe context and corrects.
  • Manual orders. Orders created in the WordPress back end and marked paid by a staff member never see the customer's browser. The CAPI pipeline still fires; the browser pipeline does not. Per-platform idempotency keeps everything consistent.
  • Pay-for-order links. Customers paying an existing order via a pay/ link hit the order-pay hook at priority 5, before WooCommerce's default handlers, so the conversion is attributed to the actual payment moment rather than to the order's original creation timestamp.
  • WooCommerce Subscriptions. First charges fire as Subscribe (or the platform equivalent). Recurring renewals fire as RecurringSubscriptionPayment. Cancellations fire as CancelSubscription. Each one with the right product context, the right value, and the right idempotency key.
  • GA4 partial and full refunds. When an order or specific line items are refunded, GA4 receives a properly-formed refund event with the correct items array.

None of these are exotic. All of them happen on every store eventually. Most of them are simply absent in competing plugins.

Why our Facebook Match Quality Score is sometimes lower than the competition (and why that is a good thing)

This one deserves its own section because it is a deliberate design decision that costs us in benchmark screenshots and that we believe is the right call anyway.

Meta's Event Match Quality (EMQ) score rewards events that include a _fbp cookie value, because in normal use that value links the server-side event to a browser session Meta has already seen. The score does not, however, validate that the _fbp value corresponds to a real browser session. It just rewards the presence of a value.

Some competing WooCommerce tracking plugins exploit this. When they cannot find a real _fbp cookie on the request, they mint a synthetic one server-side, typically constructed from the current Unix timestamp and a random ten-digit number, then send it to Meta as if it were a genuine browser identifier. The pattern looks like this:

'fb.1.' . time() . '.' . rand( 1000000000, 9999999999 )

The result is that the EMQ dashboard goes up. The result is also that:

  1. The synthetic value maps to no real Facebook user. It contributes zero attribution lift, zero audience-matching uplift, zero advantage to campaign optimization. The number on the dashboard moves; the number that pays your bills does not.
  2. Collision risk. When the real Facebook pixel later sets a genuine _fbp cookie on the same browser, the same browser is now associated with two different _fbp values. Per-browser deduplication and attribution stops working cleanly for that visitor.
  3. For ad-blocker users, the inflation is guaranteed to be useless. The whole point of using a server-side identifier for ad-blocker users is to recover an identity Meta can match. A timestamp plus a random number cannot be matched to anything, ever, by definition.

This is also not what Meta's own SDK does. The official Facebook PHP Business SDK's UserData::setFbp() is a pure setter, and Util::getFbp() returns null when the cookie is not present. Inventing a value is a third-party choice, not something the platform's own tooling endorses. Meta's own Conversions API documentation describes fbp as a value that "comes from the _fbp cookie" and gives no instructions for synthesising one.

We have considered shipping an opt-in toggle that mints synthetic _fbp values to inflate EMQ on demand for users who care about the score in isolation. We decided against it because we believe the people we owe an honest answer to are the people writing us cheques and judging the plugin by the conversions Meta's optimizer actually delivers, not by a number on a dashboard.

If you are comparing plugins by EMQ score in a side-by-side screenshot, the Pixel Manager will sometimes look worse. If you are comparing them by the number of conversions Meta's optimizer actually delivers from a fixed ad spend, the conclusion is different.

The cherry on top: edge offload

Everything above is implemented in PHP and runs on the WooCommerce server. For stores where the additional CPU and outbound HTTP load of fanning out to five or six ad platforms per browser-side event becomes meaningful, the Pixel Manager integrates with the Server-Side Proxy, which moves the fan-out to a Cloudflare Worker on a first-party subdomain of the store. Browser-side events skip the WooCommerce server entirely and go straight to the edge, where the Worker fans out to every ad platform. The events travel under your domain, so they are not blocked by browser privacy controls.

With the Server-Side Proxy active, browser-side events never touch the WooCommerce PHP server: the browser posts directly to your first-party ssp.yourshop.com subdomain, and the Cloudflare Worker handles the fan-out to every platform. Purchase events (which need order data only the shop server knows) still go from PHP, but the high-volume traffic, PageView, AddToCart, ViewContent, etc., is fully off-loaded. It is a performance and accuracy multiplier on top of an already correct implementation, not a replacement for getting the implementation right in the first place.

Where this leaves you

If you are running a single-platform, single-payment-gateway, single-checkout-style WooCommerce store, almost any tracking plugin will look like it works in your test orders. The cracks open when you add a second platform, or a customer pays via Klarna, or someone marks a manual order paid in the back end, or a subscription renews.

The Pixel Manager exists because we ran into every one of those cracks on real stores and decided to fix them once, in a shared base class, instead of patching them per-platform forever. If your store is at the size where each of those edge cases costs real money, this is the difference that matters.

Pixel Manager for WooCommerce is free to install and try, and the Server-Side Proxy is included with the paid tiers.

Interested to get updates?

Sign up to our monthly newsletter today.