# GA4 WooCommerce ecommerce tracking: a setup from scratch

**Author:** Piotr Litwa - GTM & Analytics Specialist  
**Published:** 2026-10-16  
**URL:** https://piotrlitwa.com/articles/en/ga4-woocommerce-ecommerce-tracking.html  
**Language:** en  
**Keywords:** ga4 woocommerce, woocommerce ecommerce tracking, gtm4wp woocommerce, woocommerce purchase event ga4, woocommerce data layer

---

GA4 WooCommerce ecommerce tracking means sending seven events from your store to GA4 (`view_item_list`, `view_item`, `add_to_cart`, `begin_checkout`, `add_shipping_info`, `add_payment_info`, `purchase`), each with an `items` array, and a `transaction_id` on the purchase so GA4 can tell orders apart. You can get there with a plugin, with GTM4WP and a GTM container, or with your own data layer code.

Most stores I check have the events. Far fewer have revenue that matches WooCommerce within 10%. That gap is what this guide is about: duplicate purchases when someone refreshes the thank-you page, block checkout breaking the middle of the funnel, and payment gateways that never send the customer back.

> **Key Takeaways**
> - Pick one method: a plugin for simple stores, GTM4WP plus GTM if you also run ads pixels, a custom data layer only if your checkout is heavily modified.
> - The `items` array is the heart of GA4 ecommerce. `item_id` must match your product feed, or Google Ads and Merchant Center reports won't join up.
> - The thank-you page fires `purchase` again on every refresh. Block it on the server with an order meta flag, not only by trusting GA4's deduplication.
> - The block-based Checkout (default for new stores since WooCommerce 8.3) doesn't fire the classic jQuery checkout events. `add_shipping_info` and `add_payment_info` are the first to break.
> - Check GA4 revenue against WooCommerce orders monthly, by order number. A 10-20% gap is normal in the EU because of consent. 40% isn't.

## Three ways to send WooCommerce data to GA4

There's no single right answer. There's a right answer for your store's complexity.

| Method | What it is | Good for | Watch out for |
|---|---|---|---|
| **WooCommerce's own GA extension** | "Google Analytics for WooCommerce", free from WooCommerce | One GA4 property, no other pixels | Little control over parameters, its own gtag.js can duplicate GTM |
| **GTM4WP + GTM** | Thomas Geiger's GTM plugin, pushes a GA4-format ecommerce data layer | Stores running GA4 plus Meta, TikTok, Google Ads | You still build the GA4 tags and triggers in GTM |
| **Custom data layer** | Your own PHP and JS pushes into `dataLayer` | Headless, heavily modified checkouts | You own every bug and every WooCommerce update |

A note on "Google for WooCommerce": that's the official plugin for Merchant Center and Google Ads. It's useful for the product feed and Ads conversions. It isn't what I'd use to build your GA4 ecommerce funnel.

My default for a store with paid traffic is GTM4WP plus GTM. One data layer feeds GA4, Meta, TikTok, and Google Ads from one place, and you can see every tag in one container. The plugin route is fine until the day you add a second vendor. Then you have two systems reading the cart, and they'll disagree.

Whatever you pick, use only one. A GA4 extension sending gtag.js plus a GTM container with a GA4 tag on the same measurement ID is double-tagging, and I wrote a whole piece on [Google tag vs GTM](https://piotrlitwa.com/articles/en/google-tag-vs-gtm.html) about how that happens.

## The seven events and the items array

GA4 has a fixed recommended ecommerce schema. Use the exact names. `addToCart` or `add_to_basket` lands as a [custom event](https://piotrlitwa.com/articles/en/ga4-custom-events.html) and never shows up in the Monetization reports.

| Event | When it fires in WooCommerce | Parameters I always send |
|---|---|---|
| `view_item_list` | Shop, category, search, or related products render | `item_list_id`, `item_list_name`, `items` |
| `view_item` | Single product page loads | `currency`, `value`, `items` |
| `add_to_cart` | Add to cart, including AJAX buttons on archives | `currency`, `value`, `items` |
| `begin_checkout` | Checkout page loads with a non-empty cart | `currency`, `value`, `coupon`, `items` |
| `add_shipping_info` | Customer selects a shipping method | `currency`, `value`, `shipping_tier`, `items` |
| `add_payment_info` | Customer selects a payment method | `currency`, `value`, `payment_type`, `items` |
| `purchase` | Order received page, once per order | `transaction_id`, `currency`, `value`, `tax`, `shipping`, `coupon`, `items` |

Each object in `items` should carry at least `item_id`, `item_name`, `price`, and `quantity`. I add `item_brand`, `item_category`, and `item_variant` where the store has them. GA4 accepts up to 200 items per event, which only matters for B2B stores with bulk carts.

Three decisions to make before anyone writes code:

1. **`item_id`: SKU or product ID?** Use whatever your Merchant Center feed uses, or product-level reports won't match across tools.
2. **Variations.** For a T-shirt in size M, send the variation's SKU as `item_id` and "M" as `item_variant`, or every size counts as one product.
3. **`value` on purchase.** I use the items total after discounts, with tax and shipping in their own parameters.

The exact rule matters less than applying it the same way everywhere.

Before each push, clear the previous ecommerce object with `dataLayer.push({ ecommerce: null })`. GTM4WP does this for you. Custom code tends to forget, and then `view_item` carries the items of the previous list. I go deeper on push structure in the [GA4 data layer guide](https://piotrlitwa.com/articles/en/gtm-data-layer-ga4-guide.html).

## The thank-you page problem: duplicate purchases

WooCommerce's order received page lives at `/checkout/order-received/{order_id}/?key=wc_order_...`. That URL is stable. Customers bookmark it, refresh it, come back to it from the confirmation email. Every load renders the page again, and a naive setup pushes `purchase` again.

GA4 is documented to deduplicate purchases with the same `transaction_id`. In practice I don't build on that alone. It doesn't help a Meta or TikTok purchase tag reading the same push, and I've seen duplicates get through in GA4 too.

Julia runs a ceramics store in Wrocław, around 900 orders a month. GA4 showed 1,060 purchases for September 2026 while WooCommerce had 912 paid orders. In BigQuery, 214 `transaction_id` values appeared more than once. Most were customers on Przelewy24 who paid, landed on the thank-you page, then opened the same link again from the confirmation email.

The fix is a flag on the order. Push `purchase` only if the order hasn't been tracked, then mark it:

```php
add_action( 'woocommerce_thankyou', function ( $order_id ) {
    $order = wc_get_order( $order_id );
    if ( ! $order || $order->get_meta( '_ga4_purchase_sent' ) ) {
        return;
    }
    $items = array();
    $value = 0;
    foreach ( $order->get_items() as $item ) {
        $product = $item->get_product();
        $price   = (float) $order->get_item_total( $item, false );
        $value  += $price * $item->get_quantity();
        $items[] = array(
            'item_id'   => $product ? ( $product->get_sku() ?: (string) $product->get_id() ) : '',
            'item_name' => $item->get_name(),
            'price'     => round( $price, 2 ),
            'quantity'  => $item->get_quantity(),
        );
    }
    $data = array(
        'event'     => 'purchase',
        'ecommerce' => array(
            'transaction_id' => (string) $order->get_order_number(),
            'currency'       => $order->get_currency(),
            'value'          => round( $value, 2 ),
            'tax'            => round( (float) $order->get_total_tax(), 2 ),
            'shipping'       => round( (float) $order->get_shipping_total(), 2 ),
            'items'          => $items,
        ),
    );
    echo '<script>window.dataLayer=window.dataLayer||[];dataLayer.push({ecommerce:null});dataLayer.push(' . wp_json_encode( $data ) . ');</script>';
    $order->update_meta_data( '_ga4_purchase_sent', 1 );
    $order->save();
} );
```

It uses `get_meta` and `update_meta_data`, so it works with HPOS (High-Performance Order Storage) too.

The trade-off: if the first load fails, say the browser closed mid-render, that order is never sent. I'll take a missed order over a doubled one. After the change, Julia's October numbers: 812 GA4 purchases, 931 WooCommerce orders, zero duplicate IDs. An 87% match, which is about right for her banner.

## Block checkout gotchas

Since WooCommerce 8.3 (November 2023), new stores get the Cart and Checkout blocks by default instead of the `[woocommerce_checkout]` shortcode. The block checkout is a React app talking to the Store API. Old tracking code doesn't know it exists.

Here's what breaks:

- **jQuery events are gone.** Classic checkout fires `updated_checkout` and `payment_method_selected` on `document.body`. Tracking that listens for them gets nothing in block checkout. That's how `add_shipping_info` and `add_payment_info` silently stop.
- **PHP hooks inside the form don't run.** Snippets on `woocommerce_after_checkout_form` or `woocommerce_review_order_before_payment` never fire, because the blocks don't render those templates.
- **AJAX add to cart in blocks.** Product Collection blocks on archives use their own add-to-cart flow. Check that `add_to_cart` still fires on category pages, not only on single product pages.

Szymon, who sells bike parts from Toruń, switched to block checkout in spring 2025 when he rebuilt his checkout page. His funnel exploration showed `begin_checkout` to `add_payment_info` at 0% for six weeks. Nobody noticed, because `purchase` still worked. The fix: update GTM4WP to the current version, delete a custom jQuery listener his previous developer had added, and re-test every checkout step in GTM Preview.

If you're on a custom setup, test both checkouts. If you're on a plugin, check its changelog for Checkout block support before you switch.

## Consent: what changes for a WooCommerce store

In the EU, every one of these events falls under consent. With Consent Mode v2 in Advanced mode, GA4 still receives cookieless pings for users who reject, and models some of the gap. In Basic mode, rejecting users send nothing.

On WordPress, that means a CMP that sets consent defaults before GTM loads. I covered the setup in the [Cookiebot on WordPress guide](https://piotrlitwa.com/articles/en/cookiebot-wordpress-install-guide.html), and the same logic applies to any CMP. Two WooCommerce-specific points:

- The data layer push for `purchase` can happen regardless of consent. It's just a JavaScript object. What matters is whether the tags reading it respect consent.
- Pixels outside Google (Meta, TikTok) don't read Consent Mode. Each one needs its own consent requirement in GTM.

## Verifying revenue against WooCommerce orders

This is the step that tells you whether the build works. I do it on day 7 and day 30 after launch, then monthly.

1. **Test the funnel in DebugView first.** Place a real order with a coupon and two products, using [GA4 Debug Mode](https://piotrlitwa.com/articles/en/ga4-debug-mode.html). Check every event has `items`, and that `purchase` has a `transaction_id` matching the WooCommerce order number.
2. **Export WooCommerce orders** for a closed month, statuses Processing and Completed only. Pending payment, Failed, and Cancelled aren't sales.
3. **Pull GA4 purchases by transaction ID.** In Explore, a free-form table with Transaction ID as the dimension and Purchase revenue as the metric. BigQuery is cleaner if you have the export.
4. **Join on order number.** Orders in WooCommerce but not GA4 are consent rejections, ad blockers, or customers who didn't return from the gateway. IDs in GA4 but not WooCommerce are test orders or failed payments counted too early.
5. **Look at the gap by payment method.** If one gateway has a much bigger gap, it's not sending customers back to the order received page.

A gap of 10-20% is normal for an EU store with an honest banner. Over 30%, something is broken. I wouldn't trust any GA4 revenue report until this check has been done once. If you're coming from Shopify, the mechanics differ but the check is the same as in my [GTM on Shopify guide](https://piotrlitwa.com/articles/en/google-tag-manager-shopify.html).

## Frequently asked questions

**Which plugin should I use for GA4 on WooCommerce?**
For a store with only GA4 and no other pixels, WooCommerce's free Google Analytics extension is enough. If you also run Meta, TikTok, or Google Ads tags, use GTM4WP with a GTM container, so one data layer feeds every vendor. Don't run both on the same measurement ID.

**Why does GA4 show more purchases than WooCommerce?**
Usually the thank-you page is firing `purchase` again on refresh or when customers reopen it from the confirmation email. Check for repeated `transaction_id` values. Fix it with an order meta flag so each order is pushed once.

**Why does GA4 show less revenue than WooCommerce?**
Consent rejections, ad blockers, and payment gateways where customers don't return to the order received page. A 10-20% gap is normal in the EU. Compare by payment method to find a gateway that's losing more than the rest.

**Does WooCommerce block checkout work with GA4 tracking?**
Yes, but code written for the classic shortcode checkout breaks. jQuery events like `updated_checkout` don't fire in the block checkout, so `add_shipping_info` and `add_payment_info` are the first to disappear. Use a plugin version that supports Checkout blocks, or rewrite custom listeners.

**Should item_id be the SKU or the WooCommerce product ID?**
Whatever your Merchant Center product feed uses. Matching IDs let GA4, Google Ads, and Merchant Center reports join on the same product. For variable products, use the variation's SKU or ID.

**Is transaction_id required for the purchase event?**
Technically GA4 accepts a purchase without it, but don't do that. `transaction_id` is how GA4 tells orders apart and how you'll reconcile revenue with WooCommerce. Use the order number customers see.

## Next steps

Start with the reconciliation, not the build. Export last month's paid WooCommerce orders, pull GA4 purchases by transaction ID, and compare. That one spreadsheet tells you whether you need a fix or a rebuild.

If the numbers are off by more than 30%, or you see repeated transaction IDs, the [Free GTM Audit](https://piotrlitwa.com/checkGTM/) will show what your container fires and what ignores consent. If you'd like the whole ecommerce setup built and checked against your orders, that's [GA4 implementation](https://piotrlitwa.com/services.html#ga4).

---

**Sources and further reading**
- [Measure ecommerce in GA4](https://developers.google.com/analytics/devguides/collection/ga4/ecommerce) (Google for Developers)
- [GA4 recommended events reference](https://developers.google.com/analytics/devguides/collection/ga4/reference/events) (Google for Developers)
- [GTM4WP plugin](https://wordpress.org/plugins/duracelltomi-google-tag-manager/) (WordPress.org)
- [Cart and Checkout blocks](https://woocommerce.com/document/cart-checkout-blocks-status/) (WooCommerce)

---

*Written by [Piotr Litwa](https://piotrlitwa.com/about.html) - independent GTM & Analytics specialist.*
