GA4 says one number of sales, WooCommerce says another
You open WooCommerce Analytics and see last month's orders. You open GA4's Monetization reports and the purchase count is different. Revenue doesn't match either. Meta and Google Ads report a third set of numbers. You start to wonder which channel is actually paying for itself, and whether your ad budget is going to the right place.
This is one of the most common tracking complaints on WooCommerce, and it has a short list of causes. Most of them are fixable. One part of the gap is not, and should not be: some shoppers say no to tracking, and that's their right.
Why it happens
WooCommerce records an order on the server the moment checkout completes. GA4 records a purchase only when a browser runs a tag that sends a purchase event. Anything that stops that tag from running, or makes it run twice, splits the two numbers apart.
Reasons GA4 shows fewer purchases
- Consent banners and Consent Mode. If a shopper declines analytics cookies, GA4 tags either don't fire or send cookieless pings, depending on how Consent Mode is set up. Basic mode sends nothing before consent. Advanced mode sends pings that GA4 may use for modeling, but only when the property meets Google's thresholds.
- Ad blockers and browser privacy. Blockers and some privacy-focused browsers stop requests to Google's analytics domains entirely.
- Offsite payment gateways. With PayPal, some bank transfer and buy-now-pay-later gateways, the buyer pays on another site. If they close the tab instead of returning to your
order-received page, the order exists and the purchase event never fires.
- Subscription renewals. A WooCommerce Subscriptions renewal is charged by a scheduled action on the server. No browser is involved, so no browser tag can ever record it.
- Cross-domain checkouts. If checkout or payment runs on a different domain and cross-domain measurement isn't configured, the session breaks. You may see the purchase credited to the wrong source, or lose it.
- Missing
transaction_id. Some setups drop it, and GA4 then handles the events inconsistently.
Reasons GA4 shows more purchases
- Thank-you page reloads. The buyer refreshes the page, comes back to it from the order email, or the browser restores the tab. If the tag fires on every page view, each visit is another purchase.
- Duplicate tags. A WooCommerce GA4 plugin sends the purchase, and a GTM container sends it too. Or the theme has a hardcoded
gtag snippet nobody remembers.
- Reused or fake
transaction_id. A tag that sends a fixed value, the session ID or a timestamp instead of the order number makes deduplication impossible.
- The wrong trigger. The purchase fires when the checkout button is clicked, or when the thank-you page loads for a failed or pending order, instead of when payment succeeds.
Reasons revenue differs even when counts match
- Order statuses. WooCommerce reports may include or exclude pending, on-hold, failed, cancelled and refunded orders depending on settings. GA4 keeps a purchase unless a
refund event is sent.
- What's in
value. One side includes tax and shipping and the other doesn't. Or discounts are applied in one and not the other.
- Currency. A multi-currency store that doesn't send the
currency parameter, or sends the wrong one, gets converted wrongly.
What fixing it involves
1. Reconcile order by order
I pick a closed date range, usually the last full month. I export every WooCommerce order in that range with its order number, status, total, payment method and whether it's a renewal. In GA4 I build an exploration listing every transaction_id with its purchase count and revenue for the same range, using the same time zone as the store. If the property is linked to BigQuery, I query the raw events instead, which avoids thresholds and sampling.
Then I join the two lists on order number. Every order lands in one bucket: matched once, matched more than once, missing from GA4, or present in GA4 with no real order. Missing orders get sorted by payment method and renewal flag. The pattern often points straight at the cause: all the missing ones paid by PayPal, or all the duplicates are on one device type, or every renewal is absent.
2. Watch the event fire
I place test orders with GA4 DebugView and Google Tag Assistant open. I check which tags fire on the thank-you page, how many times, from which container or plugin, and with which parameters. I reload the page and open it from the order email to see whether it fires again. I test with consent declined and accepted.
3. Fix the setup
- One source of truth. The purchase event is sent from exactly one place: either the plugin or GTM, never both. Everything else that sends
purchase gets removed.
- Correct parameters.
transaction_id is the WooCommerce order number. value, tax, shipping and currency follow one written rule, matching how you read revenue in WooCommerce.
- Fire once. The event only fires for paid statuses, and the order is marked as tracked in its meta after the first send, so reloads don't resend it. GA4 is meant to drop repeat purchases with the same
transaction_id, but I don't rely on that alone.
- Server-side purchases where the browser can't see them. Renewals and offsite payments are sent from the server, either through the GA4 Measurement Protocol when the payment completes, using the client ID saved on the order at checkout, or through a server-side tagging container. Refunds are sent as
refund events the same way.
- Consent Mode set up properly. The banner sets the default state before any tag loads and updates it on choice, with all four signals:
ad_storage, analytics_storage, ad_user_data and ad_personalization. Server-side events respect the same choice.
- Cross-domain measurement configured for any external checkout or payment domain you control.
4. Keep the gap explained
A 100% match is not the goal, and a setup that shows one is usually counting something it shouldn't. An acceptable gap is one where every missing order falls into a known bucket, mostly declined consent and blockers, with no duplicates and no GA4 purchases without a real order behind them. It should also stay roughly stable. I write down what the normal gap looks like for your store and rerun the reconciliation every month. A sudden change is the alert that a plugin update, a new gateway or a theme change broke something.
Clean purchase data matters beyond GA4. Meta and Google Ads optimize on the conversions you send them, so duplicates and gaps steer your budget. This work is the first step of my growth engineering service, because every decision about ads, pages and email is only as good as the data behind it.