Check Events Manager against your WooCommerce order list for the same week. The gap is real orders your Pixel never saw, because an ad blocker killed the script or Safari's ITP expired the cookie first. And here's what most store owners miss: WooCommerce is the best platform to fix this on. Better than Shopify in one specific way.
Why is WooCommerce better positioned for CAPI than hosted platforms?
Because you own the server. Order creation happens in PHP on a machine you control, so you can fire real server-side events directly, no SaaS middleman, no per-event pricing, no app taking a cut. Shopify routes you through its pixel sandbox or a paid app for the same thing. Why it matters:
- Reliability. A server-to-server request to Meta's Graph API can't be blocked by an ad blocker or a browser privacy feature. If WooCommerce recorded the order, Meta can get the event.
- Data quality. The buyer's real email, phone, name, and billing address sit in the order object. Exactly what Meta's Event Match Quality scoring wants.
- Cost. The Graph API is free. Your server is already paid for.
The catch: the three common implementation paths are not equal.
| Facebook for WooCommerce | Web GTM + server GTM | Custom PHP hooks |
|---|---|---|
| Low effort, low control | One dataLayer feeds every platform | Total control, zero extra tools |
| No custom parameters or events | You own the event_id and dedup | The Graph API is free |
| Funnel builders bypass its hooks | Needs an sGTM container | Keep it in a custom plugin |
| Solves Meta only | Best when you spend beyond Meta | For devs and odd checkouts |
Which WooCommerce CAPI setup should you use?
The official plugin if you want done-in-an-hour and can live with its limits; GTM plus server-side GTM if you run multiple ad platforms and want one pipe; custom hooks if you want total control with zero extra tools.
| Path | Effort | Control | Best for |
|---|---|---|---|
| Facebook for WooCommerce plugin | Low | Low. Events and dedup logic are whatever the plugin ships | Stores that need baseline CAPI working today |
| GTM web + server-side GTM | Medium-high. Needs an sGTM container | High. One dataLayer feeds Meta, Google Ads, GA4, TikTok | Stores spending across multiple platforms |
| Custom PHP hooks | Medium. You write the code | Total. Every parameter, event, and retry is yours | Developers and heavily customized checkouts |
How does the official Facebook for WooCommerce plugin handle CAPI?
The official plugin connects your store to Meta, injects the browser Pixel, and sends server events once you've run the connect wizard and picked your Pixel (dataset). For a lot of stores that's genuinely enough. Where it falls short, from stores I've audited:
- No custom parameters or custom events. What it sends is what you get.
- When something breaks (duplicate Purchases, missing content_ids on variable products), you're debugging a black box. Your only real lever is reinstalling.
- Checkout replacements and funnel builders can bypass the hooks it listens to, so Purchase events silently stop.
- It solves Meta only. Google Ads and GA4 still live in separate, browser-only setups.
My rule: run the official plugin only if you spend only on Meta and your checkout is stock. Otherwise move to one of the next two paths, and turn the plugin's Pixel and server events off when you do. Two Purchase pipelines without shared event IDs means doubled revenue in reporting.
How do you set up WooCommerce CAPI through server-side GTM?
Push order data into the dataLayer on the thank-you page, send it to a server GTM container as a GA4 event, and let a Meta CAPI tag in that container forward it to the Conversions API:
- Stand up a server container. Host on Stape or self-host on a VPS, mapped to a first-party subdomain like
ss.yourstore.com. - Push a purchase dataLayer event. On
woocommerce_thankyou, output the order: transaction ID, value, currency, items, customer data for hashing. - Send it via a GA4 event tag in web GTM, transport URL pointed at your server container.
- Add the Meta Conversions API tag (Stape's, from the sGTM template gallery) with your Pixel ID and an access token from Events Manager settings.
- Fire the browser Pixel Purchase from web GTM too, same event ID both places. Dedup is covered below, and it's the step people botch.
A minimal thank-you page push looks like this:
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: "purchase",
transaction_id: "18342", // WooCommerce order ID
value: 129.00,
currency: "USD",
items: [{ item_id: "SKU-4471", quantity: 1, price: 129.00 }],
customer_email: "buyer@example.com", // sGTM hashes before sending
customer_phone: "+15551234567"
});
More moving parts, yes. What you buy is one pipeline that also feeds Google Ads Enhanced Conversions, GA4, and TikTok Events API from the same container. Build once instead of maintaining four plugin stacks.
| woocommerce_thankyou | order_status_processing |
|---|---|
| Only fires if the buyer lands there | Fires when the order state changes |
| A refresh can run it twice | Works if the buyer closes the tab |
| Right place for the dataLayer push | Also fires for phone orders |
| Funnel plugins can replace the page | Needs _fbp and _fbc saved as meta |
How do you fire CAPI events directly from WooCommerce hooks?
Hook into the order lifecycle in PHP and POST the event straight to Meta's Graph API, no GTM involved. Two hooks matter:
woocommerce_thankyoufires when the buyer lands on the order-received page. Convenient, but only if the buyer actually reaches that page, and a refresh can run it twice.- Order status hooks like
woocommerce_order_status_processingfire when the order state changes, regardless of what the buyer's browser does. More reliable for the server event. They also fire for phone orders, so decide whether those should count as ad conversions.
The skeleton, wired to the status hook:
// PHP, in a small custom plugin — not your theme's functions.php
add_action('woocommerce_order_status_processing', function ($order_id) {
$order = wc_get_order($order_id);
if ($order->get_meta('_capi_sent')) return; // don't send twice
$payload = [
'event_name' => 'Purchase',
'event_time' => time(),
'event_id' => (string) $order_id, // dedup key
'action_source' => 'website',
'event_source_url' => $order->get_checkout_order_received_url(),
'user_data' => [
'em' => [ hash('sha256', strtolower(trim($order->get_billing_email()))) ],
'ph' => [ hash('sha256', preg_replace('/\D/', '', $order->get_billing_phone())) ],
'fbp' => $_COOKIE['_fbp'] ?? null,
'client_ip_address' => $order->get_customer_ip_address(),
'client_user_agent' => $order->get_customer_user_agent(),
],
'custom_data' => [
'value' => (float) $order->get_total(),
'currency' => $order->get_currency(),
'content_ids' => array_map(fn($i) => (string) $i->get_product_id(), array_values($order->get_items())),
'content_type' => 'product',
],
];
// POST to graph.facebook.com/vXX.X/{pixel_id}/events with your access token
// then: $order->update_meta_data('_capi_sent', 1); $order->save();
});
Two notes. Capture _fbp and _fbc (or the landing-page fbclid) at checkout and save them as order meta, because when a status hook fires from a cron or payment webhook there are no cookies to read. And keep this in a custom plugin, not your theme, so a theme update doesn't delete your tracking.
- Order 18342 placed — The order ID becomes the dedup key
- Browser Pixel fires — fbq Purchase, eventID as 4th argument
- Server event fires — event_id 18342 from the status hook
- Meta keeps one — Events Manager shows a dedup'd pair
How do you deduplicate the browser Pixel and the server event?
Send both events with the same event_name and event_id, and Meta discards the copy. For WooCommerce the natural key is the order ID: the browser sends fbq('track', 'Purchase', {...}, {eventID: '18342'}), the server sends event_id: "18342", Meta keeps one.
What breaks dedup in real audits:
- The browser uses the order ID while the server sends a random UUID. Same purchase, two "different" events, doubled reporting.
eventIDplaced inside the custom data object instead of as the fourth argument tofbq. It gets ignored and nothing deduplicates.- The official plugin left running alongside a GTM or custom setup. Its event IDs will never match yours.
Verify in Events Manager: Purchase should show both browser and server sources, with a processed count matching your orders, not double. Test Events shows dedup status per pair in near real time.
What customer data should you send for Event Match Quality?
Everything the order already contains, hashed with SHA-256 where Meta requires it: email, phone, name, city, state, zip, country, plus the unhashed _fbp and _fbc values, client IP, and user agent. Meta scores match quality on a 0-10 EMQ scale in Events Manager; richer matched parameters mean more attributed conversions from the same spend.
This is where WooCommerce quietly beats browser-only tracking: the server event reads customer data straight from the order object, already validated by the checkout, while a Pixel has to be fed it through Advanced Matching. Normalize before hashing (lowercase and trim emails, digits-only phones with country code) or the hashes won't match anything on Meta's side.
Which caching plugins and page builders break WooCommerce tracking?
Any full-page cache that serves a static copy of the order-received page, and any optimizer that delays tracking scripts. The usual suspects:
- Page caching (WP Rocket, LiteSpeed Cache, W3 Total Cache, host-level caches). Most exclude cart and checkout by default, but the order-received URL pattern sometimes isn't. A cached thank-you page serves the previous buyer's dataLayer, or none at all. Exclude
/checkout/order-received/explicitly. - JS delay and defer features. "Delay JavaScript until user interaction" is great for PageSpeed scores and terrible for tracking: a buyer who lands on the thank-you page and closes the tab never triggers the interaction, so GTM never loads and the Pixel never fires. Exclude GTM from delay lists. Your server event is the safety net here.
- Page builders and checkout replacements. Elementor, Divi, and funnel plugins that swap the default thank-you page can skip
woocommerce_thankyouentirely, taking your dataLayer push with it. If purchase events died the week someone redesigned the checkout, this is why.
After any caching or builder change, run one low-value test order and watch it land in Test Events. Five minutes. The only way to know.
Frequently asked questions
Does WooCommerce have a built-in Meta Conversions API integration?
Not in core. The free Facebook for WooCommerce plugin adds Pixel plus server events with minimal customization. For custom parameters, multi-platform tracking, or nonstandard checkouts, use server-side GTM or custom hooks.
Do I still need the Meta Pixel if I use the Conversions API on WooCommerce?
Yes, run both. The Pixel supplies _fbp and _fbc and covers events like ViewContent; the server event guarantees the Purchase arrives. Deduplicate with a shared event ID.
What should I use as the event_id for WooCommerce Purchase events?
The WooCommerce order ID, as a string, on both the browser Pixel (the eventID parameter of fbq) and the server event's event_id field. It's unique, stable, and available in both contexts.
Which hook should fire the server-side Purchase event, woocommerce_thankyou or an order status hook?
Prefer woocommerce_order_status_processing (or completed) for the server event, since it fires even when the buyer never returns to the thank-you page. Use woocommerce_thankyou for the browser-side dataLayer push.
Why is Meta showing more purchases than WooCommerce after adding CAPI?
Almost always broken deduplication: mismatched event IDs, or two plugins sending Purchase in parallel. Turn off every Purchase source except one browser-and-server pair and check dedup status in Events Manager.
Do I need Stape or another paid service for WooCommerce CAPI?
No. Custom PHP hooks send events straight to the Graph API for free, and you can self-host a server GTM container on your own VPS. Managed hosting like Stape trades money for maintenance time.
How do I test if my WooCommerce Conversions API events are working?
Open Test Events in Events Manager, add the test code to your server payload, and place a real test order. You should see the browser and server Purchase arrive as a deduplicated pair with customer parameters attached.
Does the Conversions API bypass GDPR consent requirements?
No. Server-side events still process personal data, so EEA and UK traffic needs the same consent signal your Pixel needs, on both the browser and server paths.





