How to report Shopify purchase and refunds events in GA4

Altin Gjoni

Written by Altin Gjoni

Content Strategist

How to report Shopify purchase and refunds events in GA4

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_view
  • view_item
  • add_to_cart
  • begin_checkout
  • add_payment_info
  • purchase
  • search

GA4 will not natively capture and track:

  • add_shipping info
  • view_promotion
  • select_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 refund event 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

  1. Add a product to the cart.
  2. Go to the checkout page and fill in the shipping fields.
  3. Open Inspect Elements → Console and type dataLayer.
  4. 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.

F.A.Qs on Shopify purchase and refund events


What's the best way to handle multiple currencies between Shopify and GA4?

The best way to handle multiple currencies between Shopify and GA4 is to send the native checkout currency, not a converted value. If you send mixed or converted currencies inconsistently, GA4 revenue will never line up with Shopify. If you operate in multiple markets: Send native checkout currency per event Let GA4 handle reporting and conversion Avoid manual currency conversions in GTM or the data layer If Shopify and GA4 totals still differ slightly, it's usually due to rounding or tax handling, not currency setup. The safest approach to handling multiple currencies between Shopify and GA4 is always to send GA4 the native checkout currency and the unconverted order value.

What happens during our call?

During the call, we'll review your current Shopify tracking and SEO setup, identify what's causing reporting gaps (like purchase/refund mismatches), and walk you through the highest-impact fixes. You'll leave with a clear action plan, prioritised by effort vs impact, plus next steps you can implement in-house or with our team. Book a call here

How do I track failed payments or abandoned payment attempts in GA4?

GA4 does not automatically track failed payments in Shopify. However, you can infer payment issues by tracking: add_payment_info without a subsequent purchase Checkout error events exposed via Shopify Customer Events Drop-offs between add_payment_info and purchase For most stores, this level of insight is enough to identify: Payment method friction Errors caused by shipping or taxes Region-specific checkout failures Explicit "payment failed" events require custom checkout logic and are usually only worth implementing for very high-volume stores.

How should I manage consent mode and cookie banners in Shopify so that eCommerce data remains as accurate as possible?

Consent will always affect analytics; the goal is to minimise distortion, not eliminate it. If consent is misconfigured, you'll often see a sudden drop in conversion rate and under-reported revenue. The best practices to manage consent mode and cookie banners in Shopify are: Use Google Consent Mode (v2) Fire GA4 events even when consent is denied, but without identifiers Avoid blocking GTM entirely before consent

What should I use as transaction_id: Shopify order number or order ID?

We recommend using whatever your existing purchase event is already sending as transaction_id. Consistency matters more than the choice.

Altin Gjoni

Content Strategist

Altin Gjoni is a Content Strategist who creates in-depth, actionable content for Shopify and eCommerce merchants. With a background in digital strategy and hands-on experience across multiple industries, he turns complex eCommerce challenges into clear, practical guides that help brands grow, convert, and compete.