Connecting GA4 to Shopify via the Google & YouTube app is a good start, but it often leads to revenue and refund mismatches.
This article will show you how to send clean Shopify purchase data to GA4, get the exact tag and trigger settings, and set up the correct GA4 eCommerce tracking.
Why Shopify GA4 data is often wrong
You might have probably already connected GA to Shopify via the Google & YouTube App and are doing some basic tracking. If not, simply download and install the App from the Shopify App Store.

By default, GA4 via the Google & YouTube App will natively track:
page_viewview_itemadd_to_cartbegin_checkoutadd_payment_infopurchasesearch
GA4 will not natively capture and track:
add_shipping infoview_promotionselect_promotion- Custom checkout interactions
- Refund events with item-level detail
The takeaway is that while the purchase data is sent to GA4, it’s not always trustworthy without additional configuration. Meanwhile, refund events are not tracked reliably because they happen in the Shopify Admin, and GA4 never “sees” them unless you explicitly send them.
You might have noticed a mismatch in practice when GA4 revenue differs from Shopify’s, because refunds don’t reduce revenue in GA4 correctly.
Root cause for Revenue and Refund mismatch
Revenue mismatches are caused by:
- Duplicate purchase events (Shopify + Google Tag Manager)
- Limited control over Shopify’s native GA4 payload
- Tax, shipping, and currency handling differences
- Timing and session attribution differences
Refund mismatches are caused by:
- Refund events are not sent reliably
Partial refunds missing item-level detail - Refunds issued outside the original session
- No controlled data layer for refunds
GA4 not natively tracking add_shipping_info also contributes to the potential flaws. Now let’s tackle them all one by one.
The solution: Send clean Shopify purchase data in GA4
We can fix most of the above issues by sending clean Shopify purchase data to GA4.
Purchase events are sent natively by Shopify via the Google & YouTube App and should be validated, not duplicated, to avoid revenue inflation.
Refunds require a custom implementation using GTM and a controlled data layer, as refunds are not reliably sent to GA4 by default.
add_shipping_info requires a custom Shopify Customer Events pixel and GTM forwarding, as it is not tracked natively.

An overview of the role each element plays in reporting correct data from your store
Step 1: Confirm ‘purchase’ fires once
Before jumping to the next steps, place a test order, open GA4 > Reports > Realtime, and confirm you see ‘purchase’ only once. If this is the case, skip step two.
Step 2: Remove duplicate purchase tracking
Open Google Tag Manager > Tags. Search for any tag that fires a GA4 event named “purchase”. If Shopify is already sending ‘purchase’ via Google & YouTube, disable/remove the GTM purchase tag and publish GTM.
With this step, we ensure that no duplicate purchase events are reported in GA4. Now let’s focus on refunds.
Step 3: Decide how you’ll send refunds to GA4
Shopify does not reliably send refunds to GA4 by default, so you need a custom source for getting your Shopify analytics in order. How this works is as follows:
- Create a webhook on the Shopify admin.

Shopify sends a refund webhook to an endpoint (the URL). When a refund occurs, Shopify hits your webhook URL.
Step 4: Push a refund event into the data layer

Next, translate the refund into a GA4-friendly event. This is where a data layer acting as a bridge between Shopify and GA4 is needed.
GA4 needs a certain amount of information, at a minimum, the transaction_id. The other information involved is:
- Refunded value
- Currency
- Refunded items (SKU, quantity, price)
The information is included in the data layer below
javascript
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: "refund",
ecommerce: {
transaction_id: "ORDER-12345",
currency: "USD",
value: 25.00,
items: [
{
item_id: "SKU-123",
item_name: "Product Name",
quantity: 1,
price: 25.00
}
]
}
});
This payload is generated by your backend endpoint after it receives the Shopify refund webhook and is pushed into the data layer on a page where GTM is loaded. GTM then sends the refund event to GA4 using the data-layer variables you set up in Step 5.
Step 5: Create the required Data Layer Variables (one-time setup)
This variable tells Google Tag Manager where to read the refund value from the data layer so it can be sent to GA4.
| Parameter name | Value |
|---|---|
| transaction_id | {{DLV – ecommerce.transaction_id}} |
| value | {{DLV – ecommerce.value}} |
| currency | {{DLV – ecommerce.currency}} |
| items | {{DLV – ecommerce.items}} |
Set them up by following the instructions in the screenshot below.

Step 6: Create the GTM tag to send refunds to GA4
With refunds showing on the data layer and the variables set, GTM can now send clean data to GA4. The final step is to create a GTM tag with the event name set to refund, mapped to the data-layer variables created in Step 5.
Step 7: Validate the setup
- Issue a test refund in Shopify Admin
- Open GA4 → Reports → Realtime
- Confirm a
refundevent appears and matches the original purchase
Refunds may take up to 24 hours to appear in standard GA4 reports.
Final touch: Fix the missing add_shipping_info checkout step

Shopify checkout often misses add_shipping_info, which leaves a gap in your funnel between begin_checkout and add_payment_info. Adding it helps you see where customers drop off after entering shipping details.
This is needed in practice because it completes the checkout funnel and shows what happens when a customer commits to checkout but drops before they pay.
begin_checkout
→ add_shipping_info
→ add_payment_info
→ purchase
With add_payment_info, you can effectively tell if shipping costs hurt conversion, which countries or regions drop off more, etc.
If not immediately, you will need to set it up at a certain point.
Step 1- Create a Shopify custom pixel
Navigate to Customer Events in the Shopify settings and follow the instructions in the screenshots below:


Step 2: Add the data layer code
In the Code window, paste the following:
Don’t forget to replace GTM-XXXXXXX with your GTM container ID before replacing the code
javascript
// Initialize the dataLayer
window.dataLayer = window.dataLayer || [];
(function (w, d, s, l, i) {
w[l] = w[l] || [];
w[l].push({ "gtm.start": new Date().getTime(), event: "gtm.js" });
var f = d.getElementsByTagName(s)[0],
j = d.createElement(s),
dl = l != "dataLayer" ? "&l=" + l : "";
j.async = true;
j.src = "https://www.googletagmanager.com/gtm.js?id=" + i + dl;
f.parentNode.insertBefore(j, f);
})(window, document, "script", "dataLayer", "GTM-XXXXXXX");
function logEventToConsole(dataLayerEvent) {
const customStyle01 =
"color: #FFFF00; background-color: #000000; font-size: 10px; font-weight: bold; padding: 2px 0;";
console.log("%cDataLayer Event: add_shipping_info ", customStyle01, dataLayerEvent);
}
analytics.subscribe("checkout_shipping_info_submitted", (event) => {
const getEventData = (obj, path, fallback = "") => {
return (
path.split(".").reduce((acc, part) => {
if (acc && part.includes("[")) {
const [key, index] = part.replace("]", "").split("[");
return acc[key] ? acc[key][index] : undefined;
}
return acc ? acc[part] : undefined;
}, obj) || fallback
);
};
const page_data = {
hostname: getEventData(event, "context.document.location.hostname"),
location_query_string: getEventData(event, "context.document.location.href"),
path: getEventData(event, "context.document.location.pathname"),
referrer: getEventData(event, "context.document.referrer"),
page_title: getEventData(event, "context.document.title"),
url: getEventData(event, "context.document.location.href"),
};
const user_data = {
id: event.clientId,
customer_id: getEventData(event, "data.checkout.order.customer.id"),
phone: getEventData(event, "data.checkout.shippingAddress.phone"),
email: getEventData(event, "data.checkout.email"),
address: {
city: getEventData(event, "data.checkout.shippingAddress.city"),
address: getEventData(event, "data.checkout.shippingAddress.address1"),
state: getEventData(event, "data.checkout.shippingAddress.state"),
country: getEventData(event, "data.checkout.shippingAddress.country"),
postal_code: getEventData(event, "data.checkout.shippingAddress.zip"),
first_name: getEventData(event, "data.checkout.shippingAddress.firstName"),
last_name: getEventData(event, "data.checkout.shippingAddress.lastName"),
},
language: getEventData(event, "context.navigator.language"),
userAgent: getEventData(event, "context.navigator.userAgent"),
};
const event_data = {
timestamp: event.timestamp || "",
id: event.id || "",
};
const ecommerce_data = {
currency: getEventData(event, "data.checkout.currencyCode"),
value: getEventData(event, "data.checkout.totalPrice.amount"),
items: event.data.checkout.lineItems.map((item, index) => ({
item_id: getEventData(item, "variant.product.id"),
item_name: getEventData(item, "variant.product.title"),
item_brand: getEventData(item, "variant.product.vendor"),
item_category: getEventData(item, "variant.product.type"),
price: getEventData(item, "variant.price.amount"),
item_variant:
getEventData(item, "variant.title") || getEventData(item, "variant.untranslatedTitle"),
item_list_name: "Checkout",
index: index + 1,
product_id: getEventData(item, "variant.id"),
product_image: getEventData(item, "variant.image.src"),
product_url: getEventData(item, "variant.product.url"),
product_untranslatedTitle: getEventData(item, "variant.product.untranslatedTitle"),
product_sku: getEventData(item, "variant.sku"),
quantity: getEventData(item, "quantity"),
})),
};
const dataLayerEvent = {
event: "add_shipping_info",
user_data: user_data,
event_data: event_data,
ecommerce: ecommerce_data,
page_data: page_data,
};
const newUrl = new URL(dataLayerEvent.page_data.location_query_string, window.location.origin);
const newTitle = dataLayerEvent.page_data.page_title;
if (newUrl && newTitle) {
history.pushState(null, newTitle, newUrl.toString());
}
dataLayer.push({ ecommerce: null });
window.dataLayer.push(dataLayerEvent);
logEventToConsole(dataLayerEvent);
});
Step 3: Verify the data layer
- Add a product to the cart.
- Go to the checkout page and fill in the shipping fields.
- Open Inspect Elements → Console and type dataLayer.
- Verify that the add_shipping_info dataLayer exists and is populated

Step 4: Configure the GTM tag
As we did before, create a new tag following the instructions in the screenshots below.
First, we create the tag.

Next, we create a trigger for the tag.

Step 5: Test in GA4 Real-Time
Go to the GA4 real-time events report:

After completing the checkout, or at least filling in the shipping and payment info, you can confirm that add_shipping_info is appearing in real-time events:

Finally, check the GA4 checkout journey reports the next day to ensure data is being collected correctly.
Final thoughts
Clean data isn’t optional; it’s foundational. GA4 only works when the right events are sent at the right time.
We covered an essential part of the process, yet much more can be hidden in the data you are not reporting. Opportunities to raise conversion, along with hidden slops that are killing your sales.
Book a complimentary call with our Data and SEO experts to find out in detail.