Send WooCommerce and Easy Digital Downloads refunds to Google Analytics 4
Google Analytics 4 counts the revenue of every purchase it is told about. A refund you issue in WooCommerce or Easy Digital Downloads happens in the store admin, where no tag can report it. GTM4WP sends each refund to Google Analytics 4 from your server through the Google Data Manager API, matched to the purchase it reverses. This page explains when a refund is sent, what it carries, when it is not sent and why, and how to check the result.
New in 2.1.0. Refund sending is experimental and off by default.
GTM4WP 2.0.x: refunds are not sent.
Before you start
Refund sending builds on the rest of the Data Manager setup, described on the Google Data Manager hub:
- a Google service account with the Editor role on your Google Analytics 4 property,
- a destination for that property that passes its Test button,
- and Send refunds to Google Analytics turned on, which also starts storing the Google Analytics client ID with every new order.
Only refunds of orders placed after refund sending was turned on can be sent. An older order has no client ID stored, and Google Analytics could not match its refund to anything.
When a refund is sent
Every refund recorded in WooCommerce or Easy Digital Downloads is sent, a full refund as well as a partial one. When an order is refunded in several steps, each step is sent as its own refund event.
Issuing the refund only puts it in a queue. The event is sent in the background about a minute later, so issuing a refund is never slowed down. The queue uses Action Scheduler where it is available, which is always the case with WooCommerce and with Easy Digital Downloads 3.6.5 or newer, and WP-Cron otherwise. A refund that Google has accepted is not sent again to the same destination.
What the refund event carries
| Data | Where it comes from |
|---|---|
| Transaction ID | The number of the original order with your Transaction ID prefix in front of it, the same value the purchase event reported (see the GA4 guides for WooCommerce and Easy Digital Downloads) |
| Client ID | The Google Analytics client ID that GTM4WP stored with the order |
| Value and currency | The money this refund returned, in the currency of the order, on the same basis as the purchase event. Tax and shipping are left out if the exclude tax and exclude shipping settings left them out of the purchase when it was tracked |
| Items | Each refunded line with its refunded quantity and unit price, plus the item ID, name, brand, variant, categories, affiliation and discount, built by the same code as the items of the purchase event. A whole-order refund lists every line. An item the purchase event left out, through a filter or WooCommerce’s own item visibility, is left out of the refund too |
| Shipping and tax | The shipping and tax amounts this refund returned, when they are not zero |
| Consent | The ad_user_data and ad_personalization answers stored with the order, when they were recorded |
| Time | When the refund was created in the store |
This follows Google’s own guidance. The refund event reference marks the transaction ID, value and currency as required, and Google’s article on setting up ecommerce events asks for each refunded item, whether the refund is full or partial.
Google Analytics does not copy item details from the purchase into the refund, which is why the event carries the names and categories itself. In our tests, a refunded line without them showed as “(not set)” in the item reports. A refund recorded by amount only, without naming any line, is sent without items.
No name, email address or other customer detail is part of the event.
How Google Analytics matches the refund to the purchase
Google Analytics joins the refund to the purchase through the transaction ID, and to the visitor through the client ID. Both have to be the values the purchase was reported with. Two things break that:
- Changing the Transaction ID prefix in the WooCommerce or Easy Digital Downloads settings between a purchase and its refund. The refund then carries a transaction ID Google Analytics has never seen.
- A Google Analytics 4 tag that does not fire for the destination’s measurement ID. The browser then never reports a client ID, and nothing can be sent.
When a refund is not sent, and why
GTM4WP does not send a refund that Google Analytics could not match or that your consent rule forbids, and it never fills a gap with an invented value. Every refund it does not send appears in the Recent sends list as “Not sent”, with one of these reasons:
| Reason shown | What it means | Can it be sent again? |
|---|---|---|
| No Google Analytics client ID was stored with the order | The order was placed before refund sending was turned on, the Google Analytics 4 tag did not fire, or the order was created without a visitor behind it | No |
| The buyer did not allow analytics storage, so no client ID was stored | The visitor refused analytics storage, so the browser was never allowed to keep a client ID. No setting changes this | No |
| The buyer did not allow analytics storage | A client ID exists, but the stored consent answer is no and your consent rule applies to this order | Yes, if you change the consent rule |
| No consent answer was stored with the order, and your consent rule requires one | The buyer ordered before the consent banner loaded, or the site runs no consent tool | Yes, if you change the consent rule |
| No destination is configured to send to | The destinations table was empty when the send ran | Yes, after adding a destination |
| Code on this site cancelled the event | A gtm4wp_gdm_refund_event filter cancelled it by returning an empty array | Yes |
| The refund returned no money | The refund amount was zero | No |
| The refund returned only tax or shipping that the purchase value leaves out, so it changes no revenue | The purchase was reported without tax or shipping, and the refund returned nothing else | No |
| “Only output the container on production environments” holds sending back on this non-production site | The option Only output the container on production environments is on, and WordPress reports this site as a staging or development copy | Yes, on a production site or with the option off |
| The refund could not be read back from the store | The refund or its order no longer existed when the send ran | Yes |
| The shop plugin that issued the refund was not active when the send ran | WooCommerce or Easy Digital Downloads was deactivated in the meantime | Yes, once it is active again |
The consent rule on the hub decides which orders need a consent answer at all.
Retries, and what Google reports back
A send that fails for a reason that can pass, such as a network error, a rate limit or a temporary error at Google, is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours, and only for the destinations that failed. A send that Google refuses outright, for example because the service account lost its access, is marked as failed straight away. Fix the cause, then send it again from the Recent sends list.
A refund whose background job could not be scheduled at all is listed as “Failed” with the reason “The background job could not be scheduled, so it never ran”. A retry that should have run more than 15 minutes ago, but never did, can be sent again in the same way.
In rare cases the same refund reaches Google twice, for example when the connection times out after Google already accepted the request.
An accepted send is not yet an applied one. Google processes the request in the background, which takes between half an hour and a day. GTM4WP asks Google for the outcome, first after 30 minutes and then at growing intervals for up to 24 hours, and shows the answer next to the send: “Google is still processing it”, or Google’s own status with the number of records that had errors or warnings.
The Recent sends list
Recent sends sits below the destinations table in the Google Data Manager section. It lists what the plugin sent to Google from the server, newest first, with the time, the refund, the destination, the outcome and the details. The outcome is “Accepted”, “Failed, trying again”, “Failed” or “Not sent”.
- Only what needs attention hides the sends Google accepted and applied, so the failures and the deliberate skips are what is left.
- Send again on a row queues that refund again. Send again what can be sent does the same for every failed refund and every refund skipped for a reason a setting can fix. A queued refund runs in the background within a minute, and Refresh shows what became of it. Sending again only works while Send refunds to Google Analytics is on.
The list keeps the 50 most recent entries. When it is full, the oldest successful sends and the skips no setting can fix make room first, so failures and the skips you can act on stay in the list longer. Tools → Site Health and an admin notice also report a destination whose sends keep failing, as described on the hub.
Checking the result in Google Analytics
Once Google has applied a refund, it shows in two Google Analytics metrics: Refunds, the number of refund events, and Refund amount, the refunded revenue. The quickest check is a free-form exploration with the Transaction ID dimension and the Ecommerce purchases, Purchase revenue, Refunds and Refund amount metrics. Each refunded order then shows its refund next to its purchase, under the same transaction ID.
Google’s definition of Purchase revenue subtracts refunded revenue, so the revenue in your reports goes down with every refund applied. In our tests, an order of €116.00 with a refund of €26.00 showed a Purchase revenue of €90.00 and a Refund amount of €26.00.
Known limitation: several refunds on one order
This is a limitation of Google Analytics data processing itself. When one order is refunded in several steps, Google Analytics processes only the first of them, although it accepts every one. It has been reported to Google. GTM4WP still sends every step, so no plugin update is needed once Google fixes it.
For developers
The gtm4wp_gdm_refund_event filter receives every assembled refund event exactly as it would be sent, together with the platform-neutral refund data, the refund object and the order object of the store. Return a changed event to change what is sent, or an empty array to cancel the send. Recent sends then shows “Code on this site cancelled the event”. An event without a transaction ID is not sent either.
Two more filters are described on the hub: gtm4wp_gdm_destinations changes the list of destinations at runtime, and gtm4wp_gdm_order_consent overrides the consent state stored with an order.

