AttriModel
GuidesGA4
GA416 min read·Updated July 2026

How to Set Up GA4 Ecommerce Tracking: Purchase Events & the Items Array

GA4 ecommerce reports are only as good as the data layer feeding them — and most implementations get the items array subtly wrong. This guide covers the full ecommerce event flow, the exact schema GA4 expects, and the duplicate-purchase bug that quietly doubles your revenue numbers.

How GA4 ecommerce works

Unlike GA4's automatic events, ecommerce tracking is entirely manual: you push structured data to the data layer at each step of the funnel, and GA4 turns it into monetization reports. The power is in the items array — a list of product objects that rides along with every ecommerce event and populates product-level reporting.

Get the items array right and every ecommerce report just works. Get it wrong — misspelled keys, missing currency, wrong data types — and your reports are silently broken while the page appears to work fine.

Step 1: Map the ecommerce funnel to events

GA4 has a set of recommended ecommerce events. Implement the ones that match your funnel:

  • view_item_list — a product list or category page renders
  • select_item — a user clicks a product in a list
  • view_item — a product detail page renders
  • add_to_cart / remove_from_cart — cart changes
  • begin_checkout — checkout starts
  • add_payment_info / add_shipping_info — checkout steps
  • purchase — order completed

Start with view_item, add_to_cart, begin_checkout, and purchase — those four power the core funnel report.

Step 2: Build the items array correctly

Every ecommerce event carries an items array. Here's a correct add_to_cart push. Note the exact key names — GA4 is case-sensitive and ignores keys it doesn't recognize:

window.dataLayer = window.dataLayer || [];
window.dataLayer.push({ ecommerce: null });  // clear the previous object
window.dataLayer.push({
  event: 'add_to_cart',
  ecommerce: {
    currency: 'USD',
    value: 59.99,
    items: [{
      item_id: 'SKU_12345',
      item_name: 'Running Shoes',
      item_brand: 'Acme',
      item_category: 'Footwear',
      price: 59.99,
      quantity: 1
    }]
  }
});

The item_id and item_name are the two most important fields — at least one is required for the item to show up in reports. Always include currency and value at the event level, or GA4 will not attribute revenue.

The items array field reference

Each object in the items array supports these fields. GA4 is case-sensitive and silently ignores any key it doesn't recognize, so match these names exactly:

FieldRequired?TypeNotes
item_idOne of id/namestringYour SKU / product ID
item_nameOne of id/namestringHuman-readable product name
priceRecommendednumberPer-unit price — a number, not a string
quantityRecommendednumberDefaults to 1 if omitted
currencyRecommendedstringISO code, e.g. USD — item-level or event-level
item_brandOptionalstringBrand for reporting
item_categoryOptionalstringUp to 5 levels: item_category, item_category2item_category5
item_variantOptionalstringSize, color, etc.
discountOptionalnumberPer-unit discount amount
couponOptionalstringCoupon code applied
indexOptionalnumberPosition in a list (for view_item_list)
item_list_id / item_list_nameOptionalstringWhich list the item came from

Event-level vs item-level: currency and value belong at the event level (inside ecommerce), while price, quantity, and product attributes belong on each item. For purchase, the event-level value should equal the order total (sum of price × quantity), and transaction_id is required for deduplication.

Step 3: Clear the ecommerce object between pushes

Notice the { ecommerce: null } push before the event. This is essential. GA4's data layer (version 2) merges objects across pushes, so if you don't clear ecommerce first, items from a previous event can bleed into the next one — leading to phantom products in your reports. Clear it before every ecommerce push.

Step 4: Configure the GTM tags

In Google Tag Manager, for each ecommerce event:

  • Create a Data Layer Variable reading ecommerce (this passes the whole object)
  • Create a GA4 Event tag with the matching event name
  • In the tag, tick "Send Ecommerce data" and set the source to Data Layer
  • Trigger on a Custom Event matching your data layer event name (e.g. add_to_cart)

Google Tag Manager GA4 event tag with the Send Ecommerce data option enabled and the source set to Data Layer
Enable Send Ecommerce data and set the source to Data Layer on each GA4 event tag

Step 5: The duplicate-purchase bug

This is the most common and most damaging ecommerce bug. The purchase event fires on your order confirmation page — and if a customer refreshes that page or navigates back to it, the event fires again, double-counting the order and its revenue.

Fix it by making purchase events idempotent: store the transaction_id in localStorage (or a cookie) once fired, and check before firing again.

const txnId = 'T_10042';
const fired = JSON.parse(localStorage.getItem('ga4_purchases') || '[]');
if (!fired.includes(txnId)) {
  window.dataLayer.push({ ecommerce: null });
  window.dataLayer.push({
    event: 'purchase',
    ecommerce: {
      transaction_id: txnId,
      value: 59.99,
      currency: 'USD',
      items: [{ item_id: 'SKU_12345', item_name: 'Running Shoes', price: 59.99, quantity: 1 }]
    }
  });
  fired.push(txnId);
  localStorage.setItem('ga4_purchases', JSON.stringify(fired));
}

Step 6: Verify in DebugView

Use GTM Preview and GA4 DebugView to walk the funnel. For each event confirm the items array is populated, currency and value are present, and — critically — that purchase fires exactly once even after a page refresh.

GA4 DebugView showing a purchase event expanded with its items array, currency, and value parameters
Inspect the purchase event in DebugView to confirm the items array, currency, and value all arrive correctly

Handling refunds

Revenue reporting is only accurate if you also send refunds. When an order is refunded, fire a refund event with the original transaction_id. For a full refund, the transaction_id alone is enough; for a partial refund, include the specific items and the refunded value.

window.dataLayer.push({ ecommerce: null });
window.dataLayer.push({
  event: 'refund',
  ecommerce: {
    transaction_id: 'T_10042',   // original order id
    value: 59.99,
    currency: 'USD',
    items: [{ item_id: 'SKU_12345', item_name: 'Running Shoes', quantity: 1 }]
  }
});

Refunds usually happen server-side (in your order-management system), not on a page the customer sees — which is why many teams send them via the Measurement Protocol or server-side GTM rather than the browser data layer.

Client-side vs server-side ecommerce tracking

The data-layer approach in this guide is client-side: events fire in the browser. It's the fastest to implement, but it inherits the browser's fragility — ad blockers, consent tools, and abandoned confirmation pages all cost you purchase events.

For higher accuracy, mature ecommerce setups send the purchase event server-side (from the order backend) via the GA4 Measurement Protocol or a server-side GTM container. The server always knows a real order happened, so you capture every purchase regardless of what the browser does. A common hybrid: fire browser events for the upper funnel (view_item, add_to_cart) and send the authoritative purchase server-side. If you run Meta ads too, the same server-side event stream can feed the [Meta Conversions API](/guides/how-to-set-up-meta-capi).

Common mistakes to avoid

  • Wrong key names (itemId instead of item_id) → silently dropped, empty reports
  • Missing currency → revenue not attributed
  • Not clearing ecommerce → phantom items carried between events
  • Duplicate purchases → inflated revenue and conversion counts
  • Price as a string ("59.99") → send numbers, not strings, for price and quantity

Frequently asked questions