GA4 WooCommerce Purchase Tracking: Fix Missing Revenue

GA4 WooCommerce purchase tracking audit: fix missing or duplicate purchases, gateway redirects, consent gaps, refunds and transaction_id issues before BFCM.

GA4 WooCommerce Purchase Tracking: Fix Missing Revenue

Your WooCommerce dashboard shows one revenue figure and GA4 shows a lower one. If your GA4 WooCommerce purchase tracking is missing sales, or occasionally counting them twice, you cannot trust any channel or campaign report built on it.

Some gap between the two is normal. Ad blockers, consent refusals and phone orders all keep purchases out of GA4, even when tracking works perfectly.

Other gaps mean something is broken. Duplicate tags, off-site payment gateways, empty transaction IDs and stale cached thank-you pages are the usual causes.

This guide shows you how to tell the two apart. You will see how a purchase travels from checkout to GA4, where each tracking method fails, and how to set a baseline gap for your own store.

It is written for WooCommerce store owners, marketers and in-house SEOs who need revenue data that is good enough for attribution, without trying to match the bank statement to the penny.

Key Takeaways

  • WooCommerce is your source of truth for revenue; GA4 is for attribution.
  • GA4 showing fewer sales than WooCommerce is often expected.
  • GA4 showing more purchases than WooCommerce usually means duplicate purchase events.
  • Always send the WooCommerce order number as a non-empty transaction_id.
  • Off-site gateways lose purchases when customers never return to the thank-you page.
  • Server-side tracking needs the stored client_id to keep attribution.
  • Measure your own baseline gap weekly and investigate changes, not imperfection.

Why GA4 revenue never exactly matches WooCommerce orders (and what gap is acceptable)

Store owners usually notice the problem in the same way. WooCommerce shows one revenue figure, GA4 shows a lower one, and nobody knows which to trust.

Before you change any tracking, you need to know which differences are expected and which ones mean something is broken. Fixing a gap that is actually normal wastes time, and it can create new problems such as duplicate purchase tags.

Sources of truth: WooCommerce orders vs GA4 events

GA4 is a measurement tool, not an accounting system. It records events that a browser or server sends to it. If an event is never sent, GA4 never learns that the sale happened.

WooCommerce is the source of truth for money. Every paid order exists in your database whether or not the customer’s browser reported it. Use these screens for real revenue:

  • WooCommerce > Orders for individual orders, statuses and payment methods.
  • WooCommerce > Analytics > Revenue for gross sales, returns, coupons, net sales, taxes and shipping over a date range.
  • WooCommerce > Analytics > Settings for the order statuses that are counted and the date type the reports use (date created, paid or completed). Check this before you compare anything.

Use GA4 for the questions WooCommerce cannot answer: which channel, campaign, landing page or device led to the sale. You need purchase tracking that is good enough for attribution. You do not need it to match the bank statement.

Question Use WooCommerce Use GA4
How much did we sell last month? Yes No
How much was refunded? Yes Only if refunds are sent to GA4
Which channel drove the most revenue? No Yes
Which landing pages lead to purchases? No Yes
Is the tracking healthy? Yes, as the baseline Yes, compared against WooCommerce

Gaps you should expect vs gaps that signal a broken setup

Some gaps are built into how browser tracking works. Others point to a fault you can fix.

Legitimate causes of GA4 showing less than WooCommerce:

  • Consent refusals. If a visitor declines analytics cookies and you use basic consent mode, nothing is sent for that visitor, including the purchase.
  • Ad blockers and privacy browsers. Many of these block Google Analytics and Google Tag Manager scripts outright.
  • Manual, phone and admin-created orders. An order created in wp-admin never loads a thank-you page in the customer’s browser.
  • Refunds that were never sent. WooCommerce deducts the refund. GA4 still shows the original revenue unless a refund event reached it.
  • Time zone differences. If the GA4 property time zone (Admin > Property details) is different from Settings > General > Timezone in WordPress, orders near midnight land on different days.
  • Currency differences. GA4 converts each purchase into the property’s reporting currency. A multi-currency store will not match a single-currency WooCommerce report line for line.
  • Customers who never return from an off-site gateway. The payment succeeds, but the customer closes the tab before the order-received page loads.

Red flags that mean the setup is broken:

  • GA4 revenue or purchases higher than WooCommerce. Consent and blockers can only remove data, so the usual cause is duplicate purchase events, for example a plugin and a custom GTM tag both firing.
  • Zero GA4 purchases for one payment gateway while other gateways track normally. This usually points to a redirect or return-URL problem.
  • Revenue that is always off by the tax or shipping amount. The value parameter is built differently from the WooCommerce figure you are comparing it with.
  • (not set) transaction IDs in a Transaction ID exploration. Purchases are being sent without an ID, which breaks deduplication and refunds.
  • A sudden step change after a plugin update, theme change, new cookie banner or new checkout plugin.

“If GA4 shows fewer sales than WooCommerce, the tracking plugin is broken.”

Not necessarily. A gap in this direction is expected on any site with a cookie banner and visitors who use ad blockers. The plugin can be working perfectly and still never receive a chance to fire, because the browser blocked the script or the visitor refused consent. What matters is whether the gap is stable and explainable, not whether it is zero.

Decision rule: set your own baseline

Do not rely on a universal “normal” percentage. The right gap depends on your audience, region, consent setup, gateways and how many manual orders you take. A UK store with a strict consent banner and a Bangladeshi store that takes many phone orders will have very different gaps.

Build a baseline instead:

  1. Pick a recent week with no known tracking changes. Audit it using the steps later in this guide.
  2. Exclude manual and admin-created orders from the WooCommerce count.
  3. Compare paid orders in WooCommerce with purchases in GA4 for the same dates and the same time zone.
  4. Record the gap for order count and revenue separately.
  5. Recheck weekly. Investigate when the gap moves noticeably from your baseline, not because it isn’t zero.
Order gap % = (WooCommerce paid orders − GA4 purchases) ÷ WooCommerce paid orders × 100

Negative result = GA4 has MORE purchases than WooCommerce → look for duplicates first.

If you also see large amounts of unattributed revenue in GA4, that is a separate problem. Read GA4 Unassigned Traffic: Find and Fix the Causes after you finish this audit.

How a WooCommerce purchase gets into GA4

You cannot fix missing or duplicate revenue until you know which path your purchase event takes. Each path fails in different places, so the right fix depends on the method you use.

Here is the typical browser-based flow:

1. Customer completes checkout
2. Payment gateway processes payment (on-site, or off-site via redirect)
3. Customer lands on the order-received endpoint (/checkout/order-received/{order_id}/?key=...)
4. A plugin or GTM pushes a purchase event with the order data
5. gtag.js (directly or via GTM) sends the event to GA4
6. GA4 processes it and the purchase appears in reports

Note that GA4 ecommerce events are not part of enhanced measurement. Nothing is collected automatically. Something on your site has to send the purchase event.

Client-side: thank-you page, woocommerce_thankyou and the dataLayer

Most WooCommerce setups send the purchase from the customer’s browser. The trigger is the order-received page, which WooCommerce renders through the woocommerce_thankyou action hook. Plugins hook into this point, read the order, and print tracking code or a dataLayer push.

A typical GTM-style push looks like this. The schema comes from Google’s ecommerce guide (Google Analytics developer documentation):

<script>
  window.dataLayer = window.dataLayer || [];
  dataLayer.push({ ecommerce: null }); // clear the previous ecommerce object
  dataLayer.push({
    event: "purchase",
    ecommerce: {
      transaction_id: "10452",
      value: 89.50,
      tax: 14.92,
      shipping: 5.00,
      currency: "GBP",
      coupon: "WINTER10",
      items: [
        {
          item_id: "SKU-2231",
          item_name: "Merino Wool Scarf",
          price: 42.25,
          quantity: 2
        }
      ]
    }
  });
</script>

Three details matter here:

  • transaction_id must be the WooCommerce order number and never an empty string. Google warns that GA4 deduplicates every purchase sent with transaction_id="" (Google Analytics Help).
  • currency is needed whenever value is set. Use the order currency, not the store’s default, on multi-currency sites.
  • Decide what value includes, such as tax and shipping, and document it so your comparisons with WooCommerce use the same definition.

Client-side tracking only works if three things are true at the same moment: the browser loads the order-received page, consent for analytics has been granted (or your consent mode setup allows a ping), and no blocker stops the tag. If any of these fails, that purchase is lost.

Heavy third-party scripts on the thank-you page can also delay or break tags. A third-party script audit often turns up conflicting pixels and old tracking snippets.

Server-side: Measurement Protocol and server GTM

Server-side tracking sends the purchase from your server rather than the customer’s browser. There are two common routes:

  • GA4 Measurement Protocol: WordPress sends an HTTP request to Google when an order status changes, for example to processing or completed. You need the measurement ID and an API secret created in Admin > Data streams > [your web stream] > Measurement Protocol API secrets.
  • Server-side Google Tag Manager (sGTM): the browser or your server sends data to your own tagging server, which forwards it to GA4. This is more flexible but needs hosting and maintenance.

Server-side purchase events have clear strengths:

  • They fire on the order status change, so the customer does not need to return from PayPal or another off-site gateway.
  • They are not affected by browser ad blockers, because the request leaves your server.
  • They suit refunds, which happen in wp-admin where no customer browser is involved.

The trade-off is attribution. A Measurement Protocol purchase needs the visitor’s GA4 client_id, captured from the _ga cookie during the session and stored on the order. Without it, GA4 receives the revenue but cannot link it to the session, channel or campaign that produced it.

Server-side sending also does not remove your consent obligations. If a visitor refused analytics consent, take legal advice before you send their purchase from the server.

Order meta to store at checkout for server-side tracking:
_ga_client_id     → e.g. 1234567890.1712345678 (parsed from the _ga cookie)
_ga_session_id    → optional, improves session attribution
_ga_purchase_sent → flag set after sending, prevents re-sending on later status changes

Where each method can fail

Use this table to identify your current method and its weak points. Feature details for the plugins are limited to what their own documentation states. Always confirm against the current version you have installed.

Method Trigger point Refund support Duplicate risk Skill needed
Google Analytics for WooCommerce (official free plugin) Browser, after checkout. From v2.4.4 it no longer sends purchase for failed or cancelled orders (WordPress.org) Check the plugin documentation for your version High if a GTM purchase tag or second plugin also runs Low
WooCommerce Google Analytics Pro When the order is processing or completed. Covers on-site and off-site gateways (WooCommerce docs) Refund and cancellation tracking listed on the product page Low on its own. Installing it deactivates the free plugin, but custom GTM tags remain Low to medium
GTM4WP + Google Tag Manager Browser, on the order-received page via the dataLayer Not via the browser. Needs a separate server-side solution Medium. Page reloads and leftover tags are common causes Medium
Custom gtag.js on woocommerce_thankyou Browser, on the order-received page Only if you build it High unless you flag orders as already tracked Medium to high (PHP)
Measurement Protocol / server GTM Server, on order status change Yes, sent with the original transaction_id High if a browser purchase also fires with no shared ID High

The most common failures, by stage:

  1. Gateway stage: the customer never returns from an off-site payment page, so no browser event fires.
  2. Thank-you page stage: the page is reloaded, bookmarked or reopened from an email and fires the purchase again. Or a caching plugin serves a stale version.
  3. Browser stage: consent is denied or a blocker stops gtag.js or GTM.
  4. Tag stage: two tools both send a purchase, or the payload is missing transaction_id or currency.
  5. Server stage: the client_id is missing, so revenue arrives unattributed, or both browser and server send the same order.

If you are reviewing tracking in the run-up to Black Friday, update the official plugin to the latest version first. Then pair this audit with your sale-page work in Black Friday SEO for WooCommerce. If you would rather have someone check the setup with you, you can book a free consultation.

Step 1: Audit every tag that can send a purchase event

Most GA4 revenue problems on WooCommerce start with not knowing what is actually sending the purchase event. You cannot fix missing or doubled revenue until you know every system that can fire a purchase.

WordPress makes it easy to end up with several. A plugin was installed years ago, an agency added a GTM tag, Site Kit was connected later, and the theme might still hold an old gtag snippet. Each of these can send its own purchase.

Inventory plugins, GTM, Site Kit and theme code

Work through this checklist on the live site. Keep a simple note of each item and what it sends.

  1. List every active plugin that touches analytics. Go to Plugins > Installed Plugins and look for:
    • Google Analytics for WooCommerce (the official free plugin)
    • WooCommerce Google Analytics Pro
    • Google for WooCommerce
    • Site Kit by Google
    • GTM4WP (Google Tag Manager for WordPress)
    • PixelYourSite, or any other “all-in-one pixel” or tracking plugin
    • Cookie or consent plugins that inject tags themselves
  2. Record what each one sends. Open each plugin’s settings and note whether it loads a Google tag, which measurement ID or tag ID it uses, and whether ecommerce or purchase tracking is switched on. For the official plugin, the settings are usually under WooCommerce > Settings > Integration. Confirm this in your current admin, as menus change.
  3. Check the GTM container. In Tag Manager, open Tags and search for “purchase”. Look for GA4 Event tags with the event name purchase, and also for a generic GA4 Event tag using {{Event}} that fires on ecommerce events. Either one can send purchases.
  4. Search the theme and child theme code. Look in functions.php, header.php and any “header scripts” box in the theme options for gtag('event','purchase', gtag("event", "purchase" or a hard-coded G- measurement ID.
  5. Count the purchase hits for a single order. Place a test order, then on the order-received page open DevTools. Check the page source for gtag and dataLayer calls, then open the Network tab and filter on collect?v=2. Look for en=purchase.
  6. Repeat with a page reload. Refresh the order-received page and check whether another purchase fires. Customers do reload and bookmark this page.

One detail catches people out in the Network tab. gtag can batch several events into one request, so en=purchase may sit in the request payload rather than the URL. Click each collect request and check the payload, not just the address.

If you use server-side GTM, the requests may go to your own subdomain instead of google-analytics.com. Filter on v=2 or your tagging domain instead.

Here is a quick way to record what you find:

Order #10482 — order-received page — first load

Source                         Measurement ID   Purchase sent?
Google Analytics for WooCommerce  G-AAAA1111      Yes
GTM container GTM-XXXX (GA4 tag)  G-AAAA1111      Yes   <-- duplicate
Site Kit                          G-AAAA1111      No (page_view only)
Theme functions.php               G-BBBB2222      Yes   <-- old property

Result: 3 purchase hits for 1 order, sent to 2 properties

If you also want to see what else is loading on checkout, a broader third-party script audit for WordPress helps you find forgotten tags and slow pixels at the same time.

Check the GA4 property and data stream

Even a single, clean purchase tag fails if it sends to the wrong place. Check these in GA4:

  • One measurement ID. Every source should use the same G- ID. Old IDs from a previous property mean revenue is split across properties.
  • The right data stream. In Admin > Data streams, confirm the web stream’s URL matches the live store domain, not staging.
  • Property time zone. In Admin > Property details (under Property settings), check the time zone matches the one WooCommerce uses. A mismatch moves late-night orders into a different day.
  • Reporting currency. In the same screen, check the currency. GA4 converts purchase values sent in other currencies into this one, so totals will not match WooCommerce exactly if the store sells in several currencies.

Before Black Friday week, update the official plugin if you use it. The 2.4.4 changelog (October 2026) says it no longer sends purchase for failed and cancelled orders. A later successful payment on the same order is still tracked (WordPress.org plugin page). Test the update on staging first.

Also note that installing GA Pro deactivates the free plugin (WooCommerce documentation). That avoids one common clash, but it does nothing about a GTM tag or theme code that also sends purchases.

“Two purchase tags is fine. GA4 will deduplicate them.”

Do not rely on this. Google says transaction IDs help deduplicate purchases, but its help page does not give a guaranteed window. Deduplication also only applies to web streams (Google Analytics Help). Fix duplicates at the source.

Decision rule: keep exactly one system responsible for the purchase event. Choose the plugin or GTM, not both. If you choose GTM, turn off purchase tracking in every plugin, and keep plugins only for the dataLayer they push. If you choose a plugin, pause or delete the GTM purchase tag and remove theme code.

Step 2: Validate the purchase payload with dataLayer examples and DebugView

Once only one system sends purchases, check that what it sends is correct. A purchase that arrives with a bad ID, a missing currency or a text value is almost as harmful as one that never arrives.

The canonical reference for the schema is Google’s GA4 ecommerce developer guide. Check your setup against it, not against plugin blogs.

A correct WooCommerce purchase dataLayer push

If you track with GTM, the order-received page should push something like this. The first line clears any previous ecommerce object so old items do not leak into the purchase.

window.dataLayer = window.dataLayer || [];
dataLayer.push({ ecommerce: null });  // clear the previous ecommerce object
dataLayer.push({
  event: "purchase",
  ecommerce: {
    transaction_id: "10482",      // WooCommerce order number
    value: 84.50,                 // number, not a string
    tax: 7.68,
    shipping: 6.95,
    currency: "AUD",              // ISO 4217 code
    coupon: "BFCM20",
    items: [
      {
        item_id: "TSHIRT-BLK-M",  // same ID as your SKU / Merchant Center feed
        item_name: "Organic Cotton T-Shirt",
        item_variant: "Black / M",
        item_category: "T-Shirts",
        price: 32.00,
        quantity: 2
      },
      {
        item_id: "CAP-NVY",
        item_name: "Canvas Cap",
        price: 20.50,
        quantity: 1
      }
    ]
  }
});

In GTM, a single GA4 Event tag with event name {{Event}} and ecommerce data set to use the data layer can send this. Fire it on a custom event trigger that matches purchase.

Use the same item_id as your product feed. That keeps GA4, Google Ads and Merchant Center talking about the same products. If your feed IDs are messy, sort them out first with the WooCommerce GTIN, MPN and brand guide.

Now compare a broken payload. Every line marked here causes a real reporting problem:

dataLayer.push({
  event: "purchase",              // no ecommerce:null before it
  ecommerce: {
    transaction_id: "",           // empty string: GA4 treats all such purchases as one
    // transaction_id: "sess_8f3a91c" // session/random ID: breaks dedup and refunds
    value: "$84.50",              // string with a currency symbol
                                  // currency missing entirely
    items: []                     // empty items array
  }
});

The empty string is the worst one. Google’s help page warns that GA4 deduplicates all purchase events that have transaction_id="" (Google Analytics Help). Every order after the first one sent with an empty ID can disappear from revenue. This often happens when a template variable fails to load and outputs nothing.

Random or session-based IDs cause the opposite problem. A reload creates a new ID, so the same order counts twice. Refunds also cannot match the original purchase. Google adds that the same transaction ID should not be reused for different users.

Required and recommended parameters

Parameter Status WooCommerce source Notes
transaction_id Required for purchase Order number Never empty. If you use a sequential order number plugin, use the same number everywhere, including refunds.
value Recommended Order total Send as a number. Decide whether it includes tax and shipping, and document it.
currency Required when value is set Order currency Use the order’s currency, not the store default, on multi-currency stores.
tax, shipping Optional Order tax and shipping totals Numbers only.
coupon Optional Applied coupon codes Useful for checking BFCM codes.
items Required Order line items Each item needs item_id or item_name. Include price and quantity.

The “value includes tax and shipping or not” choice matters for reconciliation later. Pick one definition and compare GA4 with the matching WooCommerce figure, otherwise you will chase a gap that is only a definition difference.

DebugView and Tag Assistant checks

DebugView shows events from debug-enabled browsers in near real time. Use it with Tag Assistant so your test session is flagged for debugging.

  1. Open Tag Assistant, enter the store URL and connect. This turns on debug mode for that browser session.
  2. In GA4, go to Admin > Data display > DebugView.
  3. Place a test order. Use a staging site, a test or sandbox payment mode, or a 100%-discount coupon.
  4. Wait for the order-received page to load fully, then find purchase in the DebugView timeline.
  5. Click the event and check each parameter, then open the items section and check every product.
  6. Reload the order-received page and confirm a second purchase does not appear.

A 100%-discount coupon is quick, but it skips the payment gateway. It will not test off-site redirects such as PayPal. Run at least one test through each real gateway in test mode before big sale periods.

Test orders go into your real reports too. Cancel or refund them in WooCommerce and note their order numbers so you can exclude them when reconciling.

Check in DebugView Expected Fix if wrong
Number of purchase events Exactly 1 per order, none on reload Go back to Step 1 and remove the extra source.
transaction_id Matches the WooCommerce order number Map the variable to the order number, not a session or random value.
value Number matching your chosen order total Strip symbols and send a number. Check whether tax and shipping are included.
currency Order currency code, e.g. GBP, USD, AUD, BDT Add currency, or read it from the order on multi-currency stores.
tax and shipping Match the order screen Map to order totals, not cart estimates.
coupon Coupon code used Map the applied coupon codes.
Items count Same number of lines as the order Check the items loop and the ecommerce:null clear.
item_id SKU or feed ID Use the same ID as your Merchant Center feed.
Item price and quantity Numbers matching the order lines Send unit price as a number and the actual quantity.
Measurement ID Your live property Replace old or staging IDs.

If everything matches here but revenue is still low, the cause is usually upstream: gateway redirects, order statuses or consent. The next sections cover those.

If you would rather have someone check the full setup before Black Friday, you can book a free consultation.

Fix duplicate purchase events

Duplicate purchases inflate revenue, conversion rate and ROAS. They also hide real problems, because an inflated total can cancel out purchases you are losing elsewhere.

Find the duplicates first, then remove their cause. Do not rely on a report filter or on GA4 to clean them up.

Find duplicates with a Transaction ID exploration

A free-form exploration shows every transaction ID GA4 received and how many purchases each one recorded.

  1. In GA4, open Explore and start a blank exploration. Set the technique to Free form.
  2. Set the date range to the last 30 days. If you changed tracking recently, use a range that starts after the change.
  3. Under Dimensions, click + and import Transaction ID.
  4. Under Metrics, import Purchases (shown as Ecommerce purchases in some properties) and Purchase revenue.
  5. Drag Transaction ID into Rows. Drag both metrics into Values.
  6. Sort by Purchases, descending.
  7. Set Show rows to 500 or more so the table is not cut off.

Then read the top of the table.

What you see What it means What to do
Transaction ID with Purchases = 1 Normal Nothing
Transaction ID with Purchases of 2 or more Duplicate purchase Find the order in WooCommerce and check which tags fired
(not set) with many purchases Purchases sent without a transaction ID Fix the tag or plugin that sends no ID
IDs that don’t match WooCommerce order numbers A second tag is using another value, such as an order key or a random string Find that tag and remove it or correct it

Pick three or four duplicated IDs and open those orders in WooCommerce. Compare the payment method, device and order date. A pattern often appears, such as one gateway or iOS Safari only.

“GA4 deduplicates transaction IDs, so duplicates don’t matter.”

This is only partly true. Google says transaction ID deduplication works for web streams only and only for purchases from the same user (Google Analytics Help). A second purchase from another tag, browser or device can still be counted.

Google also warns against sending an empty string. GA4 treats every purchase with transaction_id="" as the same transaction, so real orders get collapsed together.

Google’s help page does not state a deduplication time window. Some third-party articles quote one, but you should not plan around it. Fix duplicates where they start.

Common causes on WooCommerce stores

  • A plugin plus a GTM tag. The official Google Analytics for WooCommerce plugin sends purchase, and an agency’s GTM GA4 Event tag sends it again. This is the most common cause.
  • Two plugins. For example, a pixel or marketing plugin with GA4 enabled running next to the official plugin, or Site Kit adding gtag while another plugin also tracks ecommerce.
  • Thank-you page reloads. The customer refreshes the order-received page, returns to it with the back button, or reopens the tab later. Mobile browsers often restore old tabs.
  • Bookmarks and shared links. The order-received URL contains the order ID and key, so it loads again whenever someone revisits it.
  • Emailed order-received links. Some gateways, custom emails or customer service replies include the order-received URL. Every click can fire purchase again if your code does not check.
  • The old add_to_cart double-fire. This does not affect purchase, but it inflates your funnel. The official plugin’s v2.4.4 changelog says add_to_cart now fires once instead of twice when “Redirect to the cart page after successful addition” is off (WordPress.org).

Fixes that work at the source

1. Keep one source of purchase events. Using your Step 1 inventory, choose either the plugin or GTM for ecommerce. Turn off purchase tracking in every other source. If old theme snippets or orphaned plugins turn up, our third-party script audit guide explains how to remove them safely.

2. Use the real order number as transaction_id. Use the same number the customer sees in WooCommerce. Do not use a timestamp, a random ID or an empty value. With one ID per order, reconciliation becomes a simple lookup.

3. Add a fire-once flag to custom code. If you push the purchase yourself, record that it was sent. Order meta is the most reliable option because it works on any device. A cookie only protects the same browser.

add_action( 'woocommerce_thankyou', 'mhr_ga4_purchase_push', 20 );

function mhr_ga4_purchase_push( $order_id ) {
    if ( ! $order_id ) {
        return;
    }
    $order = wc_get_order( $order_id );

    // Skip if already sent, or if the payment did not succeed.
    if ( ! $order || $order->get_meta( '_ga4_purchase_sent' ) ) {
        return;
    }
    if ( $order->has_status( array( 'failed', 'cancelled', 'pending' ) ) ) {
        return;
    }

    $items = array();
    foreach ( $order->get_items() as $item ) {
        $product = $item->get_product();
        $items[] = array(
            'item_id'   => $product ? ( $product->get_sku() ?: (string) $product->get_id() ) : '',
            'item_name' => $item->get_name(),
            'price'     => (float) $order->get_item_total( $item, false ),
            'quantity'  => (int) $item->get_quantity(),
        );
    }

    $payload = array(
        'event'     => 'purchase',
        'ecommerce' => array(
            'transaction_id' => (string) $order->get_order_number(),
            'value'          => (float) $order->get_total(),
            'tax'            => (float) $order->get_total_tax(),
            'shipping'       => (float) $order->get_shipping_total(),
            'currency'       => $order->get_currency(),
            'items'          => $items,
        ),
    );

    $order->update_meta_data( '_ga4_purchase_sent', time() );
    $order->save();

    echo '<script>window.dataLayer = window.dataLayer || [];'
       . 'dataLayer.push({ ecommerce: null });'
       . 'dataLayer.push(' . wp_json_encode( $payload ) . ');</script>';
}

Note the trade-off. The flag is saved when the page renders, not when GA4 receives the hit. If an ad blocker or denied consent stops the tag, the order is still marked as sent. Section 9 covers server-side options that close this gap.

Whether value should include tax and shipping is a reporting decision, covered in section 8. Check the full payload against Google’s ecommerce developer guide, including currency, which is required whenever value is set.

4. Update the official plugin. If you use Google Analytics for WooCommerce, update it to the latest version before Black Friday. Check the settings under WooCommerce > Settings > Integration, and confirm the path in your current admin UI.

Optional: check with BigQuery. If BigQuery export is linked, this query lists every transaction ID with more than one purchase event. The export holds raw events, so treat it as the strictest view.

SELECT
  ecommerce.transaction_id,
  COUNT(*) AS purchase_events,
  SUM(ecommerce.purchase_revenue) AS revenue_sent
FROM `your-project.analytics_123456789.events_*`
WHERE event_name = 'purchase'
  AND _TABLE_SUFFIX BETWEEN '20261001' AND '20261105'
GROUP BY 1
HAVING COUNT(*) > 1
ORDER BY purchase_events DESC;

Fix missing purchases: gateway redirects, order statuses and off-site payments

Duplicates inflate revenue. Missing purchases do the opposite, and they are harder to notice because nothing looks broken. The order appears in WooCommerce, but GA4 never sees it.

On most stores, a large share of missing purchases comes from two things: payment gateways and order statuses.

Gateways that never return to the thank-you page

Most browser-based tracking sends purchase when the order-received (thank-you) page loads. If the customer never reaches that page, the purchase is never sent.

On-site gateways, such as card fields embedded in checkout, usually keep the customer on your site. Off-site gateways send the customer away to pay, and that is where things go wrong:

  • The customer pays on PayPal, a bank page or a buy-now-pay-later provider, then closes the tab.
  • The gateway shows its own “payment successful” screen and only returns if the customer clicks a button.
  • The return redirect times out or fails on a slow mobile connection.
  • The payment is confirmed in the background through a webhook or IPN, after the customer has already left.

Bangladeshi stores should check local gateways such as SSLCommerz, bKash and other mobile banking or card gateways in the same way. Return behaviour depends on the gateway, the plugin version and your settings, so check yours rather than assuming.

Test every gateway with a real order

  1. List every payment method enabled under WooCommerce > Settings > Payments.
  2. Open Tag Assistant and GA4 DebugView (Admin > Data display > DebugView).
  3. Place a real low-value order with each gateway. Use a live payment, not sandbox mode, because return flows can differ.
  4. Complete the payment, then wait. Do not click “Return to merchant” unless a normal customer would have to.
  5. Record whether you reached order-received and whether purchase appeared in DebugView with the correct ID and value.
  6. Refund the test orders afterwards and note their IDs so you can exclude them later.

Keep a log like this. The rows below are an example format. Your own results will differ.

Gateway Returns to site automatically? Purchase fired? Fix
Embedded card (on-site) Yes Yes None needed
PayPal Only after the customer clicks a button Only on return Enable auto-return; add a status-based backup
BNPL provider Yes, with a delay Yes, but sometimes the order is still pending Fire on processing, not page load
Local gateway (e.g. SSLCommerz / bKash) Record what you see Record what you see Check gateway return URL and IPN settings
Bank transfer (BACS) Yes (no payment step) Yes, before payment arrives Decide whether on-hold orders count as revenue

Failed, pending and retried payments

WooCommerce order statuses tell you whether money actually arrived. Your purchase event should follow them.

Status What it means Should purchase fire?
Pending payment Order created, payment not confirmed No
Failed Payment declined or errored No
On hold Awaiting manual payment, such as a bank transfer Your decision; be consistent
Processing Paid, awaiting fulfilment Yes
Completed Paid and fulfilled Yes, unless already sent at processing
Cancelled Cancelled by admin, customer or timeout No

The normal path is pending → processing → completed. Problems start when tracking fires on page load regardless of status.

Retries are the classic trap. A card fails, the order is marked failed, and the customer pays later using the “Pay for order” link. Same order ID, second attempt. If the failed attempt already sent purchase with that ID, the real payment can be ignored or double-counted.

The official plugin’s v2.4.4 changelog addresses this. It no longer sends purchase for failed and cancelled orders, so a later successful payment on the same order is still tracked (WordPress.org). This is another reason to update before Black Friday week.

Fixes for missing purchases

  • Enable the gateway’s auto-return. Many gateways have a setting that sends the customer back to your return URL automatically. Check the gateway account and the WooCommerce gateway plugin settings, and confirm in the provider’s documentation.
  • Track on order status, not page load. Hook purchase tracking to the moment payment is confirmed, for example when the order moves to processing. This captures customers who never return. It requires server-side sending, covered in section 9.
  • Use GA Pro’s completed purchase approach. The WooCommerce Google Analytics Pro documentation says its completed purchase event covers both off-site and on-site gateways, fires when an order is processing or completed, and tracks order currency (WooCommerce.com). Installing Pro deactivates the free plugin, so do not run both.
  • Keep a fire-once rule. If you add a status-based sender alongside the browser tag, use the same transaction ID and an order-meta flag. Otherwise you swap a missing-revenue problem for a duplicate one.

Payment gateway referrals stealing attribution

When a customer does return from an off-site gateway, GA4 can record the gateway as the referrer. The purchase is then credited to paypal.com / referral instead of the ad or search visit that brought the customer. The GA Pro documentation notes that PayPal can appear as the referrer after an off-site payment.

Add every gateway domain from your test log to the unwanted referrals list:

  1. Go to Admin > Data streams and open your web stream.
  2. Click Configure tag settings, then Show more.
  3. Open List unwanted referrals and add each gateway domain, such as paypal.com and your local gateway’s payment domain.

The change only applies to new data. It does not correct past reports. Our GA4 unassigned traffic guide covers referral exclusions and session breaks in more detail.

If you are preparing for a sale period, run the gateway tests alongside the work in our Black Friday SEO for WooCommerce guide. Then you will know your sale-week revenue is real before any campaign goes live.

If your tags are clean and you still see fewer purchases than orders, look at consent next. On stores with UK or EU customers, consent is often the largest single cause of the gap.

Some of that gap is legitimate and expected. Some of it comes from a misconfigured banner that blocks purchases even from customers who accepted cookies. Your job is to tell the two apart.

Basic vs advanced consent mode and purchases

Consent mode has two implementations. They behave very differently when a customer declines analytics cookies.

Behaviour Basic consent mode Advanced consent mode
Google tags before a consent choice Blocked until the user interacts with the banner Load with default consent set to denied
What is sent when analytics consent is denied Nothing. The purchase is not recorded. Cookieless pings without identifiers
Purchase recorded when consent is granted Yes, in full Yes, in full
Behavioural modelling Not available in the same way, because denied users send no data Possible, but only if Google judges the property eligible
Modelled data in BigQuery export No No. Only observed events are exported.

Sources: (OWOX), (Clickport). Check the current eligibility rules on Google’s “Behavioral modeling for consent mode” help page in the Analytics Help Centre before you make decisions based on them.

Third-party sources say modelling needs roughly 1,000 events per day from users who denied consent, plus a similar volume of consented users, sustained over several days. Treat these numbers as approximate until you have confirmed them with Google.

“Advanced consent mode will recover all the purchases we lose to the cookie banner.”

It will not. Advanced mode sends cookieless pings. Whether those pings ever become modelled purchases in your reports depends on eligibility, and the individual transactions are never restored with their transaction IDs.

Get legal advice before you switch to advanced mode. Tags that load and send pings before consent raise questions under UK GDPR, PECR and the EU’s ePrivacy rules. Your lawyer or data protection adviser makes that call, not your analytics setup.

Check your banner isn’t blocking the purchase tag

A customer who accepted cookies on the product page should be tracked on the order-received page. If they are not, your banner or GTM setup is losing purchases you are entitled to record.

These are the failure patterns to test for:

  • The banner loads after the purchase tag. If the consent platform sets its default state late, the purchase tag either fires without a consent state or is blocked because consent reads as denied. In GTM, the consent default must fire on the Consent Initialization – All Pages trigger, before any other tag.
  • The purchase tag waits for a consent update that never comes. Some setups fire GA4 tags only on a custom event such as consent_update. Many banners push that event only when the user clicks. A returning customer who already accepted never clicks on the order-received page, so the purchase never fires. The stored consent has to be applied on every page load.
  • GTM consent checks are set incorrectly. GA4 tags have built-in consent checks for analytics_storage. If someone also added ad_storage under Additional Consent Checks → Require additional consent for tag to fire, users who accept analytics but decline advertising lose their purchase.
  • Payment gateway returns reset consent. If the customer lands back on a different subdomain after an off-site payment, the banner may read them as a new visitor.
  • Script blockers in optimisation plugins. “Delay JavaScript” features can hold back the consent script or the GTM container on the thank-you page. Exclude both from delay and combine settings.

To test this, run a real order with Tag Assistant connected:

  1. Open the store in a private window and accept all cookies.
  2. Connect Tag Assistant from GTM preview mode and place a low-value test order.
  3. On the order-received page, select the purchase event and open the Consent tab. analytics_storage should show Granted for both the on-page default and the update.
  4. Confirm the GA4 tag is listed under Tags Fired, not Tags Not Fired.
  5. Repeat with analytics accepted and advertising declined, and then with everything declined.
Expected results
Accept all                  → purchase tag fires, full payload
Analytics yes / Ads no      → purchase tag fires (GA4 only)
Reject all, basic mode      → purchase tag does not fire
Reject all, advanced mode   → cookieless ping only, no client ID

If a large share of your third-party scripts load before consent, or load in an unclear order, work through a third-party script audit for WordPress before BFCM.

When behavioural modelling helps and when it doesn’t

Modelling fills gaps in aggregate reports for eligible properties. It does not help you reconcile individual orders.

  • Small stores may not qualify at all. A shop with a few hundred sessions a day is unlikely to meet the volume thresholds. You simply have fewer purchases in GA4.
  • Modelled data is not in BigQuery. If you reconcile orders in BigQuery, you only see observed purchases. Your BigQuery totals will be lower than the GA4 interface for the same period.
  • Modelling never creates transaction IDs. Your Transaction ID exploration will always show fewer IDs than WooCommerce has orders.

Ad blockers and privacy browsers are a separate loss. They block requests to Google’s tag domains whatever the consent state. You cannot fix this in the browser, and you should not try to disguise tags to get around a user’s choice.

Server-side tracking does not exempt you from consent. Moving the purchase hit to your server or to a server-side GTM container changes where the request comes from, not your legal obligations. If a customer declined analytics, your server should not send their purchase with an identifier. Store the consent state with the order and check it before you send anything. Section 9 covers how.

Refunds, cancellations, currency and tax settings

A purchase recorded correctly can still give you the wrong revenue later. Refunds that never reach GA4, mismatched currencies and inconsistent tax handling all cause drift. During a sale week, the drift grows quickly.

Sending refund events with the original transaction_id

GA4 does not know about a WooCommerce refund unless something sends a refund event. Clicking Refund on an order in the WooCommerce admin changes your shop’s data only.

Refunds happen in the back office with no customer browser involved. That means the event has to come from one of these:

  • Your server, via the Measurement Protocol. A small custom function hooks into WooCommerce’s refund action and posts the event to GA4.
  • WooCommerce Google Analytics Pro, which lists refund and cancellation tracking among its features (WooCommerce.com). Installing Pro deactivates the free Google Analytics for WooCommerce plugin, so check your other settings after you switch.

The refund must use the same transaction_id as the original purchase. Google requires a transaction ID on ecommerce events to process refunds properly (Google Analytics Help).

For the Measurement Protocol, you need your measurement ID and an API secret. Create the secret under Admin → Data streams → [your web stream] → Measurement Protocol API secrets.

POST https://www.google-analytics.com/mp/collect?measurement_id=G-XXXXXXX&api_secret=YOUR_SECRET

Validate first (returns errors, records nothing):
POST https://www.google-analytics.com/debug/mp/collect?measurement_id=G-XXXXXXX&api_secret=YOUR_SECRET

Here is a partial refund for one item from order 10482:

{
  "client_id": "1234567890.1730880000",
  "events": [
    {
      "name": "refund",
      "params": {
        "transaction_id": "10482",
        "currency": "GBP",
        "value": 24.99,
        "items": [
          {
            "item_id": "SKU-HOOD-BLK-M",
            "item_name": "Black Hoodie",
            "price": 24.99,
            "quantity": 1
          }
        ]
      }
    }
  ]
}

Notes on this payload:

  • Full refund: send transaction_id, currency and value and leave out items.
  • Partial refund: include only the refunded items and quantities.
  • client_id should be the customer’s GA client ID from the original session. Save the _ga cookie value to the order meta at checkout. Without it, the event is not tied to the original user.
  • Value sign: third-party guides disagree on whether refund values should be positive or negative. Follow the refund example in Google’s ecommerce developer guide, not a blog post. The example above uses a positive value, but confirm it against the guide before you deploy.
  • Backdating: some sources say Measurement Protocol events can only be backdated by about 72 hours using timestamp_micros (ACCS). Confirm the current limit in Google’s Measurement Protocol reference. Most refunds happen days later, so send them at the time of the refund rather than backdating them to the purchase date.

If the customer declined analytics consent at checkout, you will have no client ID for them. Decide with your adviser whether to skip their refunds or send them without identifiers.

Currency and multi-currency stores

The currency on the purchase event must match the currency the order was placed in. Google requires currency whenever you send value (Google Analytics developer guide).

Multi-currency plugins are the usual cause of mismatches. Depending on how they hook into WooCommerce, the tracking plugin may read either of these:

  • the base store currency, which is the WooCommerce default, or
  • the displayed currency the customer actually paid in.

The bad combination is a value in one currency labelled with another. For example, a customer pays €50 and GA4 receives value: 50, currency: "GBP". Every euro order is then overstated by the exchange rate.

Google Analytics Pro tracks the order currency (WooCommerce.com). Whatever plugin you use, test it:

  1. Switch the storefront to a non-base currency and place a test order.
  2. In Tag Assistant or DebugView, check that currency and value match the order total shown in WooCommerce for that order.
  3. Check the order’s currency on the admin order screen. It should be the same code.

GA4 converts every purchase to the property currency set under Admin → Property details. Expect small differences against WooCommerce when you reconcile foreign-currency orders, because GA4’s exchange rate will rarely match your gateway’s.

Tax and shipping: what value should include

GA4 has separate tax and shipping parameters on the purchase event. value is whatever your setup puts there. Plugins differ, and some include tax and shipping while others don’t.

Pick one definition, keep it consistent, and reconcile against the WooCommerce figure that uses the same definition. In WooCommerce → Analytics → Revenue, check the report’s own column definitions. Net sales typically excludes tax and shipping, while Total sales includes them.

If GA4 value includes… Reconcile against Watch out for
Items only, after discounts Net sales (before refunds) Coupons applied at cart level, not item level
Items + shipping + tax Total sales Tax-inclusive prices (common in AU and UK) being counted twice
Mixed (changes between plugins) Nothing reliable Fix this first. Note the date of the change in your reporting.

For ROAS, many stores prefer value without tax, because tax is not revenue. Whatever you choose, document it so that whoever reconciles in January uses the same rule.

WooCommerce order states vs what GA4 should record

Order status What GA4 should record Notes
Pending payment No purchase The customer has not paid yet
Failed No purchase Google Analytics for WooCommerce v2.4.4 stops sending purchase for failed orders (WordPress.org)
Failed, then paid on retry One purchase, on the successful payment Same transaction ID, sent only once
On hold (bank transfer, cheque) Your choice. Document it. If you count it, send a refund if the payment never arrives
Processing Purchase GA Pro fires its completed purchase here or at Completed
Completed Purchase (if not already sent) Never send a second purchase on the status change
Cancelled before payment No purchase v2.4.4 also stops sending purchase for cancelled orders
Cancelled after a purchase was recorded Full refund event Otherwise the revenue stays in GA4
Refunded Full refund event Original transaction ID
Partially refunded Refund event with items Only the refunded lines

If you are running the free plugin, update to the latest version before BFCM so the failed and cancelled order fixes apply. Then review its settings under WooCommerce → Settings → Integration, and confirm the menu path in your current version.

If you are planning sale pages alongside this tracking work, see Black Friday SEO for WooCommerce. If GA4 also shows revenue under an unclear channel, the GA4 unassigned traffic audit covers that separately. If you would rather have someone check your setup before the sale, you can book a free consultation.

Server-side purchase tracking options

Browser tags fail in predictable ways. Ad blockers stop them, customers close the tab before the order-received page loads, and off-site gateways never return some buyers. Server-side tracking sends the purchase from your server when the order is confirmed, so it does not depend on what happens in the browser.

It adds cost and maintenance, though. It is worth considering when:

  • Reconciliation shows a large, steady gap after you have fixed tags, gateway redirects and consent issues.
  • Many orders come through off-site gateways where customers often do not return to your thank-you page.
  • You need refunds in GA4. Refunds happen in the admin, not the browser, so they are usually sent server-side with the original transaction ID.
  • You run a headless or custom checkout where plugin-based browser tags do not fit.

If your gap is mostly from consent denials, server-side tracking is not the fix. Do not use a server-side setup to send data for customers who declined analytics cookies. Store the customer’s consent choice with the order and respect it, and get legal advice for UK and EU traffic.

Measurement Protocol from WooCommerce hooks

The GA4 Measurement Protocol lets your server send events directly to a GA4 web stream. It is the lightest server-side option because it runs inside WordPress.

The essentials:

  1. Create an API secret in GA4 at Admin > Data streams > (your web stream) > Measurement Protocol API secrets. You also need the stream’s measurement ID (G-XXXXXXX).
  2. Capture the browser’s client ID at checkout. Read the _ga cookie when the order is created and save it in order meta. Without it, GA4 cannot link the purchase to the visitor’s session and source, and revenue lands without useful attribution.
  3. Fire on a paid status. woocommerce_order_status_processing suits most stores. Add completed as well if you sell virtual products that skip processing.
  4. Guard against repeats. An order can move to processing more than once, for example on-hold to processing after a manual payment check. Set a “sent” flag in order meta and check it first.
  5. Test on the validation endpoint first. https://www.google-analytics.com/debug/mp/collect returns validation messages. The live endpoint, /mp/collect, accepts malformed payloads without telling you anything is wrong.

A _ga cookie value looks like GA1.1.1234567890.1712345678. The client ID is the last two number groups: 1234567890.1712345678.

Illustrative sketch only. It shows the structure. It is not production code. Add error handling and logging, check consent, use block checkout hooks if you use the Cart and Checkout blocks, and match the value basis you chose in the previous section.

<?php
// ILLUSTRATIVE ONLY – test on staging and the validation endpoint first.

// 1. Save the GA client ID when the order is created (classic checkout).
add_action( 'woocommerce_checkout_create_order', function ( $order ) {
    if ( ! empty( $_COOKIE['_ga'] ) ) {
        $parts = explode( '.', sanitize_text_field( wp_unslash( $_COOKIE['_ga'] ) ) );
        if ( count( $parts ) >= 4 ) {
            $order->update_meta_data( '_ga4_client_id', $parts[2] . '.' . $parts[3] );
        }
    }
} );

// 2. Send the purchase once, when the order becomes paid.
add_action( 'woocommerce_order_status_processing', function ( $order_id, $order ) {
    if ( $order->get_meta( '_ga4_mp_sent' ) ) {
        return; // already sent
    }
    $client_id = $order->get_meta( '_ga4_client_id' );
    if ( ! $client_id ) {
        return; // decide your own fallback; no client ID = poor attribution
    }

    $items = array();
    foreach ( $order->get_items() as $item ) {
        $items[] = array(
            'item_id'   => (string) $item->get_product_id(),
            'item_name' => $item->get_name(),
            'quantity'  => $item->get_quantity(),
            'price'     => (float) $order->get_item_total( $item, false ),
        );
    }

    $body = array(
        'client_id' => $client_id,
        'events'    => array( array(
            'name'   => 'purchase',
            'params' => array(
                'transaction_id' => (string) $order->get_order_number(), // same ID as any browser tag
                'currency'       => $order->get_currency(),
                'value'          => (float) $order->get_total(), // use your agreed basis
                'items'          => $items,
            ),
        ) ),
    );

    wp_remote_post(
        'https://www.google-analytics.com/mp/collect?measurement_id=G-XXXXXXX&api_secret=YOUR_SECRET',
        array( 'body' => wp_json_encode( $body ), 'headers' => array( 'Content-Type' => 'application/json' ) )
    );

    $order->update_meta_data( '_ga4_mp_sent', 1 );
    $order->save();
}, 10, 2 );

Two points to check in Google’s Measurement Protocol documentation before you go live. First, find out how far back an event timestamp is allowed to go if you plan to send delayed events. Second, read the current guidance on passing session_id so purchases attach to the right session. Follow the payload schema in the GA4 ecommerce guide. When you set value, currency is required.

Server-side GTM

Server-side Google Tag Manager is a tagging container that runs on a server you control, usually on a subdomain such as metrics.yourstore.com. The browser sends one stream of data to your container, and the container forwards it to GA4, Google Ads and other platforms.

  • It has hosting costs. You pay for Google Cloud or a managed sGTM host, and costs rise with traffic. Check current pricing with your provider. BFCM traffic spikes will show up on that bill.
  • It centralises purchases and refunds. WooCommerce can post order and refund webhooks to the container, and the container sends them on to every platform with the same transaction ID.
  • It is not automatically blocker-proof. Most setups still start with a browser request. A first-party subdomain helps, but the reliable path for purchases is still a server-to-server call triggered by the order.
  • It needs ongoing ownership. Someone has to maintain clients, tags, scaling and consent logic.

Hybrid setups without double counting

Most stores end up with a hybrid: browser tags for browsing and checkout events, and a server for purchases and refunds. The rule is simple:

“GA4 deduplicates by transaction ID, so it’s fine to send the purchase from both the browser and the server.”

Not quite. Google says transaction ID deduplication applies to web streams and that you should never send an empty ID (Google Analytics Help). Google does not promise how reliably or for how long it deduplicates. If both senders fire, they must use the identical transaction_id. Even then, choose one primary sender for purchases and switch the other off, then confirm in the plugin settings that its purchase event is actually disabled.

Factor Browser only (plugin/GTM) Measurement Protocol from hooks Server-side GTM Hybrid (browser + server purchase)
Cost Lowest Low, mostly developer time Ongoing hosting plus setup Highest combined effort
Attribution quality Good when the thank-you page loads Good only if client ID is captured Good; depends on setup Good if IDs and client ID align
Blocker / redirect resistance Weak Strong for purchases Moderate to strong Strong
Refund tracking Plugin-dependent Yes, via refund hooks Yes, via webhooks Yes
Maintenance Plugin updates Custom code to maintain through WooCommerce and PHP updates Container and server upkeep Highest; two paths to keep in sync
Double-count risk Plugin + GTM overlap Status changes re-firing Duplicate clients/tags Highest if not controlled

If you add custom PHP, plan for upgrades. Hook code that breaks during an update of the PHP version on WordPress and WooCommerce fails silently, and you may only notice weeks later in your reports.

Reconciliation table and a BFCM-week tracking checklist

Tracking is never “done”. A plugin update, a new gateway or a banner change can quietly change your numbers. A short weekly reconciliation catches problems before they distort a month of reporting. Before peak weeks such as Black Friday to Cyber Monday (27–30 November this year), make it daily.

Build a weekly GA4 vs WooCommerce reconciliation

Keep one sheet with one row per day. Use the same revenue basis on both sides: either totals including tax and shipping, or net product revenue. This is the basis you agreed in the currency and tax section.

Date WooCommerce orders (paid statuses) GA4 purchases WooCommerce revenue (matching basis) GA4 purchase revenue Gap % Duplicate IDs (not set) count Notes
[Date] [Woo orders] [GA4 purchases] [Woo revenue] [GA4 revenue] [(Woo − GA4) ÷ Woo] [IDs with >1 purchase] [Count] [Plugin update, new gateway, banner change…]
[Date] … … … … … … … …

Track gap % over time instead of judging one day. A stable gap is normal. A sudden jump is a defect.

Match transaction IDs to find exactly which orders are missing

Totals show that a gap exists. Matching individual IDs shows where it comes from.

  1. Export from GA4. In Explore, create a free-form exploration. Add Transaction ID, Device category and Country as dimensions, and Purchases and Purchase revenue as metrics. Use a short date range to reduce sampling, then export to CSV.
  2. Export from WooCommerce. Use Analytics > Orders, filtered to paid statuses, and click Download. Alternatively, use an order export plugin that includes the payment method, since the core report may not include it. Confirm the menu and columns in your version.
  3. Check the ID format. Your tag may send the order ID or the order number. Sequential order number plugins make these differ, so match like with like.
  4. Match in a spreadsheet. Use XLOOKUP or COUNTIF to flag WooCommerce orders that have no GA4 match, and GA4 IDs that appear more than once or are missing from WooCommerce.
  5. Pivot the missing orders by payment method, device and country. If most missing orders share one gateway or one country, you have found your cause.
Woo sheet column F (in GA4?):  =IF(COUNTIF(GA4!A:A, A2)>0, "Yes", "MISSING")
GA4 sheet column E (dupes):    =IF(COUNTIF(A:A, A2)>1, "DUPLICATE", "")
GA4 sheet column F (in Woo?):  =IF(COUNTIF(Woo!A:A, A2)>0, "", "NOT IN WOO – test/failed order?")

If you see unexplained source or channel problems rather than missing purchases, that is a separate issue. Use the GA4 unassigned traffic audit guide for that.

Troubleshooting by symptom

Symptom Likely cause Where to check Fix
GA4 purchases higher than WooCommerce orders Two senders (plugin + GTM tag), thank-you page reloads, or purchases sent for failed orders Exploration: Transaction ID × Purchases; Tag Assistant on the order-received page Keep one purchase sender; update the official plugin (v2.4.4 stops purchases for failed and cancelled orders)
Zero purchases for one gateway Off-site redirect where customers do not return, or the gateway skips the order-received page Missing-order pivot by payment method; test order on that gateway Enable auto-return in the gateway; send purchases server-side on paid status
Revenue off by a steady percentage Different tax or shipping basis between GA4 and your comparison Payload value, tax and shipping in DebugView Align the comparison basis; change it in one place only
(not set) transaction IDs Custom tag sending no ID or an empty string Exploration rows showing (not set); dataLayer on the thank-you page Always pass the order number as a non-empty string
Refunded orders still counted No refund event sent, or refund sent with a different ID GA4 refund metrics vs WooCommerce refunds Send refund server-side with the original transaction_id
Gap jumped after a site change Banner update, script optimiser delaying tags, or a new plugin Reconciliation notes column; Tag Assistant Exclude analytics from delay/defer rules; roll back and retest

Performance plugins that delay JavaScript are a common hidden cause. Review them alongside your third-party script audit so speed fixes do not drop purchase tags.

Pre-BFCM checklist

Peak week leaves no time for debugging. Your ads and stock decisions will depend on the reported numbers, so finish this list before your sale goes live. If you are still building sale pages, see Black Friday SEO for WooCommerce.

  • Update tracking plugins now, not on the day. The official Google Analytics for WooCommerce plugin v2.4.4 fixes double add_to_cart events and purchases sent for failed orders. Check its settings under WooCommerce > Settings > Integration.
  • Run a real test order on every gateway, including cards, PayPal, buy-now-pay-later and any local method such as bKash. Confirm each one appears once in GA4 with the correct ID, value and currency.
  • Check DebugView (Admin > Data display > DebugView) during each test, and use Tag Assistant on the order-received page.
  • Test with the cookie banner accepted and declined, and confirm the behaviour matches your consent mode setup.
  • Freeze tag and plugin changes from a few days before Black Friday until after Cyber Monday. Log any emergency change in the notes column.
  • Monitor Realtime daily during the peak. A sudden drop to zero purchases while orders keep arriving means something has broken.
  • Reconcile the day after each peak day while the data is fresh and the cause is still easy to trace.

What to check every month

  • Gap % trend against your normal range.
  • Duplicate transaction IDs and (not set) counts, which should both be zero.
  • Refunds in GA4 against WooCommerce refunds.
  • New gateways, plugins or banner changes added since last month.
  • Plugin changelogs for tracking fixes, and Measurement Protocol API secrets still active.

If the numbers still do not add up after these checks, book a free consultation and we can go through your setup together.

GA4 WooCommerce Purchase Tracking: Fix Missing Revenue — key takeaways
Key takeaways at a glance

Frequently Asked Questions

Why does GA4 show less revenue than WooCommerce?

GA4 only records purchases that a browser or server actually sends to it. Visitors who refuse analytics consent, use ad blockers, or never return from an off-site payment page are not reported. Manual admin orders and unreported refunds also widen the gap, so a stable shortfall is normal.

Why is GA4 counting duplicate WooCommerce purchases?

The usual cause is two tools sending the same purchase, such as a tracking plugin and a custom GTM tag. Thank-you pages that are reloaded, bookmarked or reopened from an email can also fire the event again. Run a Transaction ID exploration to find repeated IDs, then remove the extra tag or flag orders once they are tracked.

How do I test GA4 purchase tracking on WooCommerce?

Turn on debug mode with Tag Assistant, place a test order, and watch the purchase event arrive in Admin > DebugView. Check that transaction_id matches the order number and that value, currency and items are filled in. Repeat the test for each payment gateway you offer, because each one can fail differently.

Does PayPal or another off-site gateway break GA4 tracking?

It can. If the customer closes the tab before returning to the order-received page, the browser purchase event never fires. Check the gateway's return settings, and consider a method that sends the purchase when the order status changes on the server.

Can I track WooCommerce refunds in GA4?

Yes, but refunds happen in wp-admin, where no customer browser is involved, so a normal browser tag cannot send them. You need a plugin that supports refund tracking or a server-side refund event sent through the Measurement Protocol. The refund must use the same transaction_id as the original purchase.

How long do WooCommerce purchases take to show in GA4?

Purchases show up in Realtime and DebugView within seconds to minutes. Standard reports can take longer to process, so wait at least a full day before you compare GA4 with WooCommerce. Always compare the same dates in the same time zone.

Should I use a plugin or Google Tag Manager for WooCommerce purchase tracking?

A dedicated plugin is simpler and lower risk if you only need standard ecommerce tracking. GTM gives you more control but makes it easier to end up with duplicate or leftover tags. Whichever you choose, make sure only one method sends the purchase event.

Final Thoughts

Start by taking your revenue figures from WooCommerce and using GA4 only for attribution. Then identify the one method that sends your purchase event, and remove anything else that also sends it.

Next, place a test order through every payment gateway and confirm in DebugView that each purchase arrives once, with a real transaction_id and the correct currency.

Finally, record your baseline gap for orders and revenue, and check it every week. Investigate when the gap moves, not because it isn’t zero. Pay particular attention after plugin updates, checkout changes or a new cookie banner.

Want help applying this to your site? Run the free SEO check or book a free consultation with MD Harunur Rashid. Free SEO check · Book a free consultation

Written by

MD Harunur Rashid

MD Harunur Rashid — Digital Growth & Web Solutions Expert, a passionate SEO and Digital Marketing Consultant based in Dhaka, Bangladesh. With years of hands-on experience in Local SEO, WordPress web design, affiliate marketing, and digital strategy, I have dedicated my career to helping businesses build a strong, sustainable online presence. SEO is not just my profession — it is my passion. I love diving deep into search algorithms, uncovering growth opportunities, and turning underperforming websites into top-ranking, high-converting digital assets. Whether it is a local business trying to dominate its neighborhood market or a brand looking to scale globally, I bring the strategy, skills, and dedication to make it happen. On the web development side, I specialize in WordPress — designing clean, fast, and functional websites that not only look great but perform even better in search engines. From custom themes to plugin development, I bring both the technical and creative sides together seamlessly. Beyond my consulting work, I am the founder of Sofhor Tourism, born from my personal love of travel and nature. It is my way of helping fellow explorers discover the beauty of Bangladesh and beyond. I also proudly stand behind Tinnis Bakeshop — my wife's baking passion project that I am helping grow into a recognized brand in Bangladesh. I am hardworking, endlessly curious, and fiercely honest. I do not believe in shortcuts — only in real strategies that deliver real results. Every project I take on gets my full commitment, from start to finish. If you are looking for a digital partner who genuinely cares about your growth, let's connect.