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.
- 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
itemsarray is the heart of GA4 ecommerce.item_idmust match your product feed, or Google Ads and Merchant Center reports won't join up. - The thank-you page fires
purchaseagain 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_infoandadd_payment_infoare 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 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 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:
item_id: SKU or product ID? Use whatever your Merchant Center feed uses, or product-level reports won't match across tools.- Variations. For a T-shirt in size M, send the variation's SKU as
item_idand "M" asitem_variant, or every size counts as one product. valueon 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.
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:
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_checkoutandpayment_method_selectedondocument.body. Tracking that listens for them gets nothing in block checkout. That's howadd_shipping_infoandadd_payment_infosilently stop. - PHP hooks inside the form don't run. Snippets on
woocommerce_after_checkout_formorwoocommerce_review_order_before_paymentnever 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_cartstill 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, and the same logic applies to any CMP. Two WooCommerce-specific points:
- The data layer push for
purchasecan 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.
- Test the funnel in DebugView first. Place a real order with a coupon and two products, using GA4 Debug Mode. Check every event has
items, and thatpurchasehas atransaction_idmatching the WooCommerce order number. - Export WooCommerce orders for a closed month, statuses Processing and Completed only. Pending payment, Failed, and Cancelled aren't sales.
- 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.
- 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.
- 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.
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 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.
Sources and further reading - Measure ecommerce in GA4 (Google for Developers) - GA4 recommended events reference (Google for Developers) - GTM4WP plugin (WordPress.org) - Cart and Checkout blocks (WooCommerce)
Does GA4 revenue match your WooCommerce orders?
The Free GTM Audit checks your container, your ecommerce tags, and what fires without consent. Enter your store URL, get a report in about 10 minutes.
Start Free GTM Audit