Skip to content
HelalStart a project

GA4 purchases don't match my WooCommerce orders

WooCommerce says you had one number of orders last month and GA4 says another. Sometimes GA4 is lower, sometimes it's higher, and you no longer know which ad numbers to trust.

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.

Related service: Growth engineering

01 / Proof

Where I've done this

I'm building a WordPress and WooCommerce plugin for a US subscription supplement store that tracks street-marketing QR scans and attributes each order to its first and last touch. It is still in development.

US subscription supplement store

Building

QR attribution plugin

Problem
Street marketing had no way to prove which flyers produced orders.
What I built
QR codes with post-scan tracking and first/last-touch order attribution, built to grow with more features.
Outcome
In build for a live store; each order will carry the first and last QR scan that led to it.
Stack
  • WordPress
  • WooCommerce
  • PHP
  • React

02 / Questions

Questions owners ask

Should GA4 and WooCommerce match exactly?
No. WooCommerce is your record of money; GA4 is a measurement tool that depends on browsers, consent and scripts. Some gap is expected for any store that respects consent. The goal is a gap you can explain line by line, that stays steady month to month, and that has no duplicates. A gap that changes suddenly, or GA4 counting more purchases than you have orders, means something is broken.
Why does GA4 show more purchases than I have orders?
Almost always duplicates. The thank-you page fires the purchase again when the buyer reloads it or opens it from the order email, or two tags send it: a plugin and a GTM container, or two GTM containers. Failed or pending orders can also send a purchase if the event fires on page load instead of on payment. Checking transaction_id values in GA4 for repeats finds this quickly.
Is server-side tracking worth it for a small store?
It depends on what you're missing. If you sell subscriptions, or many buyers pay through a gateway that doesn't send them back to your site, the browser can never see those purchases, and a server-side purchase event is the only way to record them. If your gap is mostly consent and ad blockers, server-side tagging must still respect consent, so it won't close that part.
Do I need BigQuery for this?
Not to start. A date range of orders exported from WooCommerce and a GA4 exploration listing transaction_id is enough for a first reconciliation. The BigQuery export helps when you want event-level detail without sampling or thresholds, or want to automate the monthly check. Standard GA4 properties can link to BigQuery, so it's worth turning on early, since it only collects data from the day you link it.

Want me to look at your setup?

I review your site before a short discovery call and come with the questions and risks I see.

Start a projectGet the free checklist