refund
The refund event tells GA4 to subtract revenue from a previously recorded purchase. It’s the counterpart to purchase — and just as critical for accurate revenue reporting. Without refund tracking, your GA4 revenue figures will exceed your actual revenue by every refund you’ve issued.
Refunds are unusual in one respect: they often happen server-side, initiated from your admin panel or payment processor, not from user action in the browser. For server-side refunds, you need the GA4 Measurement Protocol. For client-side refund confirmation pages, you can push to the dataLayer normally.
When to fire
Section titled “When to fire”Fire refund when:
- A user completes a refund request flow on the website (client-side — use dataLayer)
- An admin issues a refund in your order management system (server-side — use Measurement Protocol)
- A payment processor webhook confirms a refund (server-side — use Measurement Protocol)
Full refund — complete dataLayer push
Section titled “Full refund — complete dataLayer push”// Full refund of a complete orderdataLayer.push({ ecommerce: null });
dataLayer.push({ event: 'refund', ecommerce: { transaction_id: 'TXN-2024-98765', // must match the original purchase transaction_id currency: 'USD', value: 142.97, // full order value being refunded tax: 28.59, shipping: 5.99, coupon: 'SUMMER15', items: [ { item_id: 'SKU-001-BLK-L', item_name: 'Classic Leather Jacket', item_brand: 'Heritage Co.', item_category: 'Apparel', item_variant: 'Black / Large', price: 89.99, quantity: 1 }, { item_id: 'SKU-047-WHT-M', item_name: 'Cotton Crew T-Shirt', item_brand: 'Heritage Co.', item_category: 'Apparel', item_variant: 'White / Medium', price: 24.99, quantity: 2 }, { item_id: 'SKU-112-TAN-34', item_name: 'Woven Leather Belt', item_brand: 'Heritage Co.', item_category: 'Accessories', item_variant: 'Tan / 34', price: 34.50, quantity: 1 } ] }});Partial refund — specific items only
Section titled “Partial refund — specific items only”For partial refunds where only some items are returned, include only the refunded items in the items array with the refunded quantities.
// Partial refund — only the jacket is being returneddataLayer.push({ ecommerce: null });
dataLayer.push({ event: 'refund', ecommerce: { transaction_id: 'TXN-2024-98765', // same transaction ID as the original order currency: 'USD', value: 89.99, // only the refunded item's value items: [ { item_id: 'SKU-001-BLK-L', item_name: 'Classic Leather Jacket', item_category: 'Apparel', price: 89.99, quantity: 1 // only 1 jacket is being refunded } ] }});Event schema
Section titled “Event schema”| Parameter | Type | Required | Description |
|---|---|---|---|
| event | string | Required | Must be "refund" |
| ecommerce.transaction_id | string | Required | The transaction ID from the original purchase event. GA4 uses this to match and subtract from the purchase. |
| ecommerce.currency | string | Required | ISO 4217 currency code. Must match the original purchase currency. |
| ecommerce.value | number | Required | The monetary value being refunded. For partial refunds, this is only the refunded amount. |
| ecommerce.tax | number | Optional | Tax amount being refunded. |
| ecommerce.shipping | number | Optional | Shipping amount being refunded (if applicable). |
| ecommerce.coupon | string | Optional | Coupon code from the original order. |
| ecommerce.items[] | Array<Item> | Optional | Items being refunded. For a full refund, include all items. For partial refund, include only refunded items. |
| items[].item_id | string | Required | Must match the item_id from the original purchase. |
| items[].item_name | string | Required | Product name. |
| items[].price | number | Optional | Unit price at time of refund. |
| items[].quantity | number | Optional | Quantity being refunded. |
Server-side refunds via Measurement Protocol
Section titled “Server-side refunds via Measurement Protocol”Most refunds are not initiated by the user in a browser — they’re triggered by admins, webhooks, or automated systems. For these, use the GA4 Measurement Protocol to send the refund event directly to GA4 from your server.
// Node.js — server-side refund via GA4 Measurement Protocolasync function sendRefundToGA4(refundData) { const payload = { client_id: refundData.clientId, // original buyer's GA4 client_id events: [{ name: 'refund', params: { transaction_id: refundData.orderId, currency: refundData.currency, value: refundData.refundAmount, items: refundData.items.map(item => ({ item_id: item.sku, item_name: item.name, price: item.price, quantity: item.refundQuantity })) } }] };
const response = await fetch( `https://www.google-analytics.com/mp/collect?measurement_id=${GA4_MEASUREMENT_ID}&api_secret=${GA4_API_SECRET}`, { method: 'POST', body: JSON.stringify(payload) } );
return response.ok;}GTM configuration
Section titled “GTM configuration”For client-side refund flows (user-initiated refund confirmation pages):
-
Create a Custom Event trigger. Event name:
refund. Name itCE - refund. -
Create Data Layer Variables for
ecommerce.transaction_id,ecommerce.currency,ecommerce.value, andecommerce.items. -
Create the GA4 Event tag. Event name:
refund. Enable Send Ecommerce data. Addtransaction_id,currency, andvalueas explicit event parameters. -
Test by verifying in GA4 DebugView — refund events should appear with the correct transaction_id and value. Check GA4 revenue reports to confirm the amount is being subtracted.
Common mistakes
Section titled “Common mistakes”Wrong transaction_id. Using a new ID, a refund-specific ID, or any value other than the exact original purchase transaction_id means GA4 cannot match the refund to the purchase. Always use the original order ID.
Not tracking partial quantities. If a customer returns 1 of 2 T-shirts, the refund event should have quantity: 1 for that item, not quantity: 2. The quantity must match exactly what’s being refunded.
Sending refunds only from the frontend. Customer-initiated returns via a web portal can use the dataLayer. Admin-initiated refunds, payment processor webhooks, and automated refunds must go through the Measurement Protocol. Relying only on frontend tracking means every admin refund is invisible in GA4.