GA4 Offline Refund Events
With GA4, triggering a purchase
event on checkout completion is straightforward1:
{
transaction_id: "T_12345_1",
value: 25.42,
tax: 4.90,
shipping: 5.99,
currency: "USD",
items: ...
}
If a user cancels their order or removes an item, you can simply send a refund
event2.
The sum of these events - purchases minus refunds - is reflected in Reports > Total Revenue
.
How can you report on refund
events which don't happen in the browser? It is fairly common for a user to cancel their order over the phone or for an order to get flagged as fraud.
You can upload a CSV of transaction_id
's3. The obvious drawback is that you need to manage refunds manually. Another caveat:
You should wait 24 - 48 hours before importing the refund data to make sure the original transaction is present in the account.
The Google Analytics Measurement Protocol for Google Analytics 4 enhances measurement for web and app streams by sending events directly to Google Analytics servers in HTTP requests. You can record server-to-server and offline interactions, and send them as Measurement Protocol events to Google Analytics, where they can be viewed in reports.4
This seems like the ideal solution for programatically sending "offline" refund
events.
How to validate the request
The Google Analytics Measurement Protocol for Google Analytics 4 does not return HTTP error codes
You will need to validate your API request using the Event Builder UI5 or by calling the validation endpoint: /debug/mp/collect
.
What data do you need to send?
The schema is roughly:
{
"client_id": "client_id",
"events": [{
"name": "refund",
"params": {
"currency": "USD",
"transaction_id": "T_12345_1",
"value": 12.21,
}
}]
}
client_id
is a unique identifier stored by GA4 in the _ga
cookie and looks something like GA1.2.908899769.1600020018
6. If you have BigQuery linked to GA4, it shows up as user_pseudo_id
.
You can retreive the client_id
from the cookie and store it in your database at checkout time, or by querying BigQuery:
SELECT
user_pseudo_id AS client_id,
params.value.string_value AS transaction_id
FROM
`MY_ANALYTICS_TABLE.events_*`,
UNNEST(event_params) params
WHERE
params.key = 'transaction_id'
AND params.value.string_value IN ("T_12345_1")
Open Questions
- Why does the
refund
event require aclient_id
, isn't thetransaction_id
enough? The CSV upload doesn't require aclient_id
. - Why is there no way to batch
refund
events for multiple users? - Do I need to wait 48 hours after a
purchase
before sending arefund
event? The CSV upload has this requirement, but it's unclear if it applies to the Measurement Protocol as well. - If I send a
refund
event before apurchase
event gets ingested, does it get discarded?
👉 A timestamp can be added using the
timestamp_micros
. However, the protocol only supports timestamps less than 72 hours ago. If you have to wait 48 hours to send a refund event, the refund timestamp might be stale by the time it reaches the API, causing the event to get rejected.
I haven't figured out how to inspect individual refund events in the GA4 UI using Explore
since the UI is shockingly bad. Instead, I'm using the BigQuery integration to verify that events are coming in correctly. The queries I've been using:
Refund events for a specific day
SELECT
user_pseudo_id,
event_timestamp AS refund_timestamp,
event_name AS refund_event_name,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'transaction_id') AS refund_transaction_id,
(SELECT COALESCE(value.int_value, value.double_value) FROM UNNEST(event_params) WHERE key = 'value') AS refund_amount
FROM
`MY_ANALYTICS_TABLE.events_*`
WHERE
event_name = 'refund'
AND event_date = '20230926'
Refund events with matching purchase events for a specific day
WITH purchases AS (
SELECT
user_pseudo_id,
event_timestamp AS purchase_timestamp,
event_name AS purchase_event_name,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'transaction_id') AS purchase_transaction_id,
(SELECT COALESCE(value.int_value, value.double_value) FROM UNNEST(event_params) WHERE key = 'value') AS purchase_amount
FROM
`MY_ANALYTICS_TABLE.events_*`
WHERE
event_name = 'purchase'
),
refunds AS (
SELECT
user_pseudo_id,
event_timestamp AS refund_timestamp,
event_name AS refund_event_name,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'transaction_id') AS refund_transaction_id,
(SELECT COALESCE(value.int_value, value.double_value) FROM UNNEST(event_params) WHERE key = 'value') AS refund_amount
FROM
`MY_ANALYTICS_TABLE.events_*`
WHERE
event_name = 'refund'
AND event_date = '20230926'
)
SELECT
r.user_pseudo_id,
r.refund_timestamp,
r.refund_event_name,
r.refund_transaction_id,
r.refund_amount,
p.purchase_timestamp,
p.purchase_event_name,
p.purchase_transaction_id,
p.purchase_amount
FROM
refunds r
LEFT JOIN
purchases p
ON
p.purchase_transaction_id = r.refund_transaction_id
-
https://developers.google.com/analytics/devguides/collection/ga4/set-up-ecommerce ↩
-
https://developers.google.com/analytics/devguides/collection/ga4/ecommerce?client_type=gtag#make_a_purchase_or_issue_a_refund ↩
-
https://developers.google.com/analytics/devguides/collection/protocol/ga4 ↩
-
The
client_id
is everything afterGA1.1.
orGA1.2.
↩