Status-Driven Meta (Facebook) Conversion Tracking with the Conversions API
How to configure the Pixel Manager so Facebook (Meta) conversions are recorded only when WooCommerce confirms the order is in a paid state, using the Conversions API as the single source of truth.
This article originally recommended the pmw_server_event_payload_facebook_purchase filter for a stricter "paid" definition. That filter never runs for purchase events, so the advice did not work. It has been replaced with pmw_skip_s2s_purchase_event below. The section on how the Conversions API is triggered has also been corrected: the transition into on-hold is a trigger too, and zero-value orders send a purchase like any other order.
When you actually need this
WooCommerce orders move through three broad outcomes after checkout. Each one is handled differently by the standard tracking setup:
- Order fails immediately. The payment gateway rejects the charge during checkout and the order goes straight to
failed. The customer never reaches the thank-you page, the browser pixel does not fire, and CAPI is not triggered (it only listens to paid-status transitions). Nothing is sent to Meta. No action needed. - Order is paid immediately. Most stripe-style and PayPal-style integrations finalize payment during checkout. The order lands on the thank-you page already in
processing(orcompleted), the browser pixel fires, CAPI fires, both share the sameevent_id, Meta deduplicates them, and the conversion is recorded once. This is the normal, ideal case. No action needed. - Order goes to
processingbut may still fail or never get paid. Bank transfers, invoices, BNPL, manual review flows, and some local payment methods land the order on the thank-you page in a state that looks successful but has not been confirmed by the gateway. The browser pixel fires immediately, so the conversion is recorded in Meta, even if the order is later cancelled or never gets paid.
The industry-standard behavior, including the Pixel Manager's default, is to track everything that does not immediately fail. The reason is simple: most stores' checkouts fall into category 2, and the small leakage from category 3 is usually outweighed by the cost of waiting (lost attribution, missed view-through windows, broken click-to-conversion paths in Meta's optimizer). Tracking on the thank-you page is fast, accurate for the common case, and matches what Meta expects.
The configuration in this article is for stores where category 3 is significant enough to distort their numbers and ad optimization, and they are willing to accept the tradeoffs to fix it.
What the Conversions API is, and what it is not
The Conversions API was not built to enable status-driven conversion tracking. Its primary purpose is tracking reliability: it sends events from your server directly to Meta, bypassing ad blockers, browser extensions, network-level filters, and increasingly aggressive browser privacy controls that suppress the browser pixel. In the standard setup, CAPI runs alongside the browser pixel as a backup, and Meta deduplicates the two via event_id.
For this niche scenario, we repurpose CAPI: instead of running it as a backup to the browser pixel, we make it the only sender for purchase events, and rely on its built-in status-driven trigger to delay the conversion until the order is actually paid.
Important limitation: the 7-day window
Meta's Conversions API only accepts events with an event_time within the last 7 days. If an order sits in processing for longer than 7 days before it is marked paid (or cancelled), the conversion can no longer be sent to Meta and is permanently lost from a tracking perspective. This is a hard cutoff on Meta's side, not something the Pixel Manager can work around.
For most payment methods this is a non-issue. For bank transfers, invoices, or other slow-confirming payments, it is a real constraint to weigh against the benefit of cleaner data.
How the Pixel Manager handles purchase events by default
For Facebook, the Pixel Manager fires the purchase event from two places:
- Browser pixel on the order received (thank-you) page, regardless of the order's payment status.
- Conversion API (CAPI) registered on
woocommerce_payment_complete, on everywoocommerce_order_status_{paid_status}transition (paid statuses come from WooCommerce'swc_get_is_paid_statuses(), typicallyprocessingandcompleted), and onwoocommerce_order_status_on-hold. A meta-key guard on the order prevents double-fires across these hooks.
Both events share the same event_id (pmw_{order_id}), which is how Facebook deduplicates the browser and server-side hits.
The implication is important: CAPI is status-driven. Failed, cancelled and refunded orders never reach Facebook through the CAPI pipeline, and neither do orders that sit in pending.
Two cases do reach Meta without a payment, and they are worth knowing before you build on this:
- On-hold orders. The transition into
on-holdis a deliberate trigger. Gateways that park an order as awaiting payment, such as crypto invoices and bank transfers, would otherwise report their conversions days late, and Meta penalises delayed reporting. On a shop where most on-hold orders are never paid, the effect is the opposite of what you want. - Zero-value orders. A free order runs through
payment_complete()into a paid status, so it fires a purchase like any other. Nothing on this path inspects the order total.
So a "status-driven" setup is one technical change, plus one optional gate: stop the browser pixel from firing the purchase event and let CAPI deliver it, and if on-hold or free orders matter in your shop, narrow the server-side trigger with the filter shown further down.
Required configuration
1. Enable Facebook CAPI
In the Pixel Manager settings, enable Facebook Conversion API. CAPI is what delivers the purchase event to Meta in this setup.
Enabling Facebook in the Pixel Manager also disables the tracking pixel from the Meta for WooCommerce plugin if it is installed. Catalog sync from Meta for WooCommerce is not affected, so product feeds keep working.
2. Suppress the Facebook browser pixel for purchase events
Add a JavaScript filter that returns null for purchase events on the Facebook pixel. Returning null from a pmw_pixel_data_{pixel} filter blocks that pixel from firing for the matched event. See the Event Filters reference for the full filter pipeline.
add_action('wp_head', function() {
?>
<script>
window._pmwq = window._pmwq || [];
window._pmwq.push(function() {
pmw.hooks.addFilter(
'pmw_pixel_data_facebook',
'my-store/status-driven-purchase',
function(pixelData, eventName) {
if (eventName === 'purchase') {
return null;
}
return pixelData;
}
);
});
</script>
<?php
}, 1);
That is the entire required change.
With the browser purchase event suppressed:
- CAPI is the only sender for purchase events, so browser/server deduplication is no longer a concern.
- All other Facebook events (
page_view,add_to_cart,view_item,begin_checkout, etc.) continue to fire from the browser as normal. - Conversions appear in Meta when WooCommerce transitions the order into a paid status. For most payment gateways this is essentially immediate. For offline or manually approved payments, the conversion is recorded when the order moves into a paid state.
Optional: stricter "paid" definition
Use this whenever the trigger list above is wider than your own definition of a conversion: to keep on-hold orders out, to keep zero-value orders out, or to count only completed on a shop where processing means "not paid yet".
The filter is pmw_skip_s2s_purchase_event. It receives the order itself, and returning true suppresses the server-side purchase for every platform, including through the SweetCode Cloud proxy, and keeps the order out of the Google Ads Conversion Adjustments feed.
add_filter('pmw_skip_s2s_purchase_event', function ($skip, $order) {
if (!$order instanceof WC_Order) {
return $skip;
}
// A free order is not a conversion.
if ((float) $order->get_total() <= 0) {
return true;
}
// Not paid yet, so not a conversion yet. This covers on-hold orders.
if (!$order->is_paid()) {
return true;
}
return $skip;
}, 10, 2);
A skipped order is not marked as already sent, so the purchase goes out normally the moment the order does reach a paid status. That is what makes this a gate rather than a permanent exclusion. To narrow it further, replace the is_paid() check with $order->get_status() !== 'completed'.
pmw_server_event_payload_facebook_purchase, which this article recommended before September 2026, never runs for a purchase. That filter chain processes browser events forwarded to the server, and its endpoint rejects purchase by design, because a purchase is always built from the order on the server. A status gate built on it fails silently.
Google Analytics 4 is the one exception: the Measurement Protocol applies pmw_server_event_payload_google_analytics_purchase to its own payload. For every other platform, use pmw_skip_s2s_purchase_event.
On those versions the server-side purchase path passed the Pixel Manager's internal order object to this filter instead of the WC_Order, so the instanceof WC_Order guard above silently skips the whole callback. Update to 1.68.0 or later, where the filter receives the WooCommerce order again.
See PHP Filters for the full reference.
Custom order statuses (e.g. partially-paid)
The reverse case also comes up: a deposit or partial-payment plugin adds a custom order status that WooCommerce does not treat as paid, so the purchase conversion never fires for those orders. To register a custom order status as paid, use WooCommerce's woocommerce_order_is_paid_statuses filter (the filter behind wc_get_is_paid_statuses()). The Pixel Manager picks it up automatically and fires purchase conversion tracking when an order transitions into that status:
add_filter('woocommerce_order_is_paid_statuses', function($statuses) {
$statuses[] = 'partially-paid'; // status slug without the `wc-` prefix
return $statuses;
});
Note that this widens what WooCommerce itself considers paid (for example $order->is_paid()), not just the Pixel Manager's conversion trigger.
Optional: manually re-fire CAPI for an order
If a custom workflow needs to push a CAPI purchase event for a specific order (for example from a custom hook), call the platform's static send_purchase_hit() method with the order object:
\SweetCode\Pixel_Manager\Pixels\Facebook\Facebook_CAPI::send_purchase_hit($order);
This is an internal method and not part of the public Pixel Manager API. It can change between releases.
Same pattern for other platforms
Every platform with a server-side counterpart in the Pixel Manager works the same way. Only the JavaScript filter name changes per platform.
| Platform | JavaScript filter | Server-side gate |
|---|---|---|
| Facebook / Meta | pmw_pixel_data_facebook | pmw_skip_s2s_purchase_event, $context is Facebook_CAPI::class |
| TikTok | pmw_pixel_data_tiktok | pmw_skip_s2s_purchase_event, $context is TikTok_EAPI::class |
pmw_pixel_data_pinterest | pmw_skip_s2s_purchase_event, $context is Pinterest_APIC::class | |
| Snapchat | pmw_pixel_data_snapchat | pmw_skip_s2s_purchase_event, $context is Snapchat_CAPI::class |
pmw_pixel_data_reddit | pmw_skip_s2s_purchase_event, $context is Reddit_CAPI::class | |
| Google Analytics 4 | pmw_pixel_data_google_analytics | pmw_skip_s2s_purchase_event, $context is Google_MP_GA4::class |
The server-side gate is one filter for every platform. Omit the $context check to gate all of them at once.
For a condensed reference of these snippets, see the Status-Driven Purchase Conversions recipe.
