Skip to content

purchase

The purchase event is the single most important event in your entire GA4 ecommerce implementation. Every other ecommerce event — view_item, add_to_cart, begin_checkout — exists to give context to this one. If your purchase event is broken, duplicated, or missing data, your revenue reporting is wrong, your ROAS calculations are fiction, and every optimization decision you make based on that data is compromised.

This is the event you triple-check.

Here is a production-ready purchase push with all standard parameters. Every real implementation should look like this — not an abbreviated version with one item and half the fields missing.

// Always clear previous ecommerce data first
dataLayer.push({ ecommerce: null });
dataLayer.push({
event: 'purchase',
ecommerce: {
transaction_id: 'TXN-2024-98765',
value: 142.97,
tax: 28.59,
shipping: 5.99,
currency: 'USD',
coupon: 'SUMMER15',
items: [
{
item_id: 'SKU-001-BLK',
item_name: 'Classic Leather Jacket',
item_brand: 'Heritage Co.',
item_category: 'Apparel',
item_category2: 'Outerwear',
item_category3: 'Jackets',
item_category4: 'Leather',
item_variant: 'Black / Large',
price: 89.99,
quantity: 1,
index: 0,
affiliation: 'Online Store',
coupon: 'SUMMER15',
discount: 13.50
},
{
item_id: 'SKU-047-WHT',
item_name: 'Cotton Crew T-Shirt',
item_brand: 'Heritage Co.',
item_category: 'Apparel',
item_category2: 'Tops',
item_category3: 'T-Shirts',
item_category4: 'Crew Neck',
item_variant: 'White / Medium',
price: 24.99,
quantity: 2,
index: 1,
affiliation: 'Online Store',
coupon: '',
discount: 0
},
{
item_id: 'SKU-112-TAN',
item_name: 'Woven Leather Belt',
item_brand: 'Heritage Co.',
item_category: 'Accessories',
item_category2: 'Belts',
item_category3: 'Leather Belts',
item_category4: 'Casual',
item_variant: 'Tan / 34',
price: 34.50,
quantity: 1,
index: 2,
affiliation: 'Partner Marketplace',
coupon: '',
discount: 0
}
]
}
});

This line is not optional:

dataLayer.push({ ecommerce: null });

GTM’s dataLayer uses a shallow merge model. If a previous event — say begin_checkout — pushed an ecommerce object, its data persists in GTM’s internal state. Without clearing, your purchase event might inherit stale data from the checkout step, or worse, the checkout event’s items could bleed into the purchase.

On single-page applications this is especially dangerous because the dataLayer never resets between “page” navigations. Always clear before every ecommerce push.

Without clearing

// ❌ Previous ecommerce data persists
dataLayer.push({
event: 'purchase',
ecommerce: {
transaction_id: 'TXN-123',
value: 99.99,
currency: 'USD',
items: [{ item_id: 'SKU-001' }]
}
});
// Risk: stale fields from begin_checkout
// may contaminate this event

With clearing

// ✅ Clean slate before pushing
dataLayer.push({ ecommerce: null });
dataLayer.push({
event: 'purchase',
ecommerce: {
transaction_id: 'TXN-123',
value: 99.99,
currency: 'USD',
items: [{ item_id: 'SKU-001' }]
}
});
// Clean — only this event's data exists
Event Schema purchase
Parameter Type Required Description
event string Required Must be "purchase"
ecommerce.transaction_id string Required Unique identifier for the transaction. Used for deduplication in GA4.
ecommerce.value number Required Total monetary value of the transaction. Must be a number, not a string.
ecommerce.currency string Required ISO 4217 currency code (e.g. "USD", "EUR", "GBP"). Required for any event with a value parameter.
ecommerce.tax number Optional Total tax amount for the transaction.
ecommerce.shipping number Optional Total shipping cost for the transaction.
ecommerce.coupon string Optional Order-level coupon code. Item-level coupons go in the items array.
ecommerce.items[] Array<Item> Required Array of purchased items. Must contain at least one item.
items[].item_id string Required SKU or unique product identifier.
items[].item_name string Required Product name. Either item_id or item_name is required — include both.
items[].item_brand string Optional Brand or manufacturer name.
items[].item_category string Optional Primary product category. Up to 5 levels (item_category through item_category5).
items[].item_variant string Optional Variant such as color, size, or style.
items[].price number Optional Unit price of the item. Must be a number, not a formatted string.
items[].quantity number Optional Number of units purchased. Defaults to 1 if omitted.
items[].index number Optional Position of the item in a list (0-based).
items[].affiliation string Optional Store or seller affiliation. Useful for marketplaces.
items[].coupon string Optional Item-level coupon code applied to this specific product.
items[].discount number Optional Monetary discount applied to the item.
Tag Configuration

GA4 - Event - Purchase

Type
Google Analytics: GA4 Event
Trigger
CE - purchase
Variables
DLV - ecommerce.transaction_idDLV - ecommerce.valueDLV - ecommerce.currencyDLV - ecommerce.taxDLV - ecommerce.shippingDLV - ecommerce.couponDLV - ecommerce.items
  1. Create a Custom Event trigger. Go to Triggers > New > Custom Event. Set the event name to purchase. Name it CE - purchase.

  2. Create dataLayer variables. You need a Data Layer Variable for each ecommerce parameter. Go to Variables > New > Data Layer Variable. Create variables for:

    • ecommerce.transaction_id (name it DLV - ecommerce.transaction_id)
    • ecommerce.value (name it DLV - ecommerce.value)
    • ecommerce.currency (name it DLV - ecommerce.currency)
    • ecommerce.tax (name it DLV - ecommerce.tax)
    • ecommerce.shipping (name it DLV - ecommerce.shipping)
    • ecommerce.coupon (name it DLV - ecommerce.coupon)
    • ecommerce.items (name it DLV - ecommerce.items)
  3. Create the GA4 Event tag. Go to Tags > New > Google Analytics: GA4 Event.

    • Measurement ID: Use your GA4 measurement ID constant variable (or your GA4 Configuration tag if using the older setup).
    • Event Name: purchase
    • Event Parameters: Map each parameter name to its corresponding Data Layer Variable. Add transaction_id, value, currency, tax, shipping, coupon, and items.
  4. Enable ecommerce data. In the tag settings under “More Settings”, check Send Ecommerce data and select Data Layer as the source. This tells GTM to automatically read the ecommerce.items array from the dataLayer and send it as the items parameter.

  5. Set the trigger. Attach the CE - purchase trigger to the tag.

  6. Test in Preview mode. Navigate to a test order confirmation page, verify the event fires, and check that all parameters appear in the GA4 DebugView.

Use this type in your frontend code to enforce correct purchase event structure at compile time:

interface EcommerceItem {
item_id: string;
item_name: string;
item_brand?: string;
item_category?: string;
item_category2?: string;
item_category3?: string;
item_category4?: string;
item_category5?: string;
item_variant?: string;
price: number;
quantity: number;
index?: number;
affiliation?: string;
coupon?: string;
discount?: number;
item_list_id?: string;
item_list_name?: string;
}
interface PurchaseEvent {
event: 'purchase';
ecommerce: {
transaction_id: string;
value: number;
currency: string;
tax?: number;
shipping?: number;
coupon?: string;
items: EcommerceItem[];
};
}
function pushPurchaseEvent(data: PurchaseEvent): void {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({ ecommerce: null });
window.dataLayer.push(data);
}
order-confirmation.js
// Assumes orderData is available from your backend (e.g., injected as JSON)
function trackPurchase(orderData) {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({ ecommerce: null });
window.dataLayer.push({
event: 'purchase',
ecommerce: {
transaction_id: orderData.orderId,
value: orderData.totalRevenue,
tax: orderData.taxAmount,
shipping: orderData.shippingCost,
currency: orderData.currency,
coupon: orderData.couponCode || '',
items: orderData.lineItems.map((item, index) => ({
item_id: item.sku,
item_name: item.name,
item_brand: item.brand,
item_category: item.category,
item_variant: item.variant,
price: item.unitPrice,
quantity: item.quantity,
index: index,
affiliation: item.seller || 'Online Store',
coupon: item.itemCoupon || '',
discount: item.discountAmount || 0
}))
}
});
}
// Call once on order confirmation page load
document.addEventListener('DOMContentLoaded', function () {
if (window.__ORDER_DATA__) {
trackPurchase(window.__ORDER_DATA__);
}
});

Duplicate purchases are one of the most common ecommerce tracking problems. A user refreshes the confirmation page, or the component re-renders, and suddenly you have two purchase events for the same order. Your revenue doubles overnight and someone in finance starts asking questions.

The most reliable approach is server-rendered: only output the dataLayer push once, using a flag that the backend controls.

// Backend injects this flag — only true on the FIRST render of this order
if (window.__PURCHASE_TRACKED__ !== true) {
window.__PURCHASE_TRACKED__ = true;
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({ ecommerce: null });
window.dataLayer.push({
event: 'purchase',
ecommerce: {
transaction_id: 'TXN-2024-98765',
// ... full event data
}
});
}

For SPAs, use sessionStorage keyed by transaction ID:

function trackPurchaseOnce(purchaseData) {
const key = `purchase_tracked_${purchaseData.ecommerce.transaction_id}`;
if (sessionStorage.getItem(key)) {
console.warn('Purchase already tracked:', purchaseData.ecommerce.transaction_id);
return;
}
sessionStorage.setItem(key, 'true');
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({ ecommerce: null });
window.dataLayer.push(purchaseData);
}

Before you ship your purchase tracking to production, verify every item on this list:

  1. transaction_id is unique per order. Check that the same ID never appears twice for different orders. Use your order system’s actual ID — do not generate one client-side.

  2. value is a number, not a string. value: 142.97 is correct. value: '142.97' is wrong. value: '$142.97' is very wrong. GA4 will silently ignore non-numeric values.

  3. currency is present and valid. Must be an ISO 4217 code (USD, EUR, GBP). Without currency, GA4 cannot calculate revenue. This is the most commonly missing parameter.

  4. items array is populated. An empty items array means GA4 records revenue but cannot attribute it to any products. Your ecommerce reports will show totals with no item breakdown.

  5. Each item has item_id and item_name. Both are technically optional (you need at least one), but in practice you need both for useful reports.

  6. Prices are unit prices, not line totals. price: 24.99 with quantity: 2 is correct. price: 49.98 with quantity: 2 is wrong — GA4 will calculate line total as 99.96.

  7. The event fires exactly once per transaction. Test page refreshes, browser back-button, and SPA re-renders.

  8. Ecommerce null clear is present. Confirm dataLayer.push({ ecommerce: null }) appears before the purchase push.

  9. Data appears in GA4 DebugView. Open GA4 > Admin > DebugView. Enable debug mode via GTM Preview or the GA Debugger extension. Verify all parameters appear correctly.

  10. Revenue matches your backend. Compare GA4 reported revenue against your actual order system for a sample of test transactions.

// ❌ No currency — GA4 ignores the value entirely
dataLayer.push({
event: 'purchase',
ecommerce: {
transaction_id: 'TXN-001',
value: 99.99,
items: [{ item_id: 'SKU-001', item_name: 'Widget', price: 99.99, quantity: 1 }]
}
});

Without currency, GA4 cannot attribute monetary value. Your ecommerce reports will show the event happened, but revenue will be zero. Always include currency on every event that has a value parameter.

On SPAs especially, failing to push { ecommerce: null } before the purchase push means stale data from previous ecommerce events (like begin_checkout or add_to_cart) can merge into your purchase event. This leads to phantom items, wrong quantities, or corrupted item arrays.

// ❌ All of these are wrong
value: '$142.97' // Currency symbol
value: '142.97' // String instead of number
value: '1,429.70' // Comma formatting
value: '142,97' // European decimal format
// ✅ Correct — plain number, no formatting
value: 142.97

GA4 expects a raw number. Any string formatting, currency symbols, or thousand separators will cause the value to be silently dropped or misinterpreted.

As covered above, page refreshes and SPA re-renders are the usual culprits. Implement client-side deduplication using sessionStorage or a useRef guard, and always include transaction_id so GA4’s server-side deduplication can catch anything you miss.

// ❌ Terrible — same ID for every order
transaction_id: 'purchase'
// ❌ Bad — generated client-side, not tied to actual order
transaction_id: Date.now().toString()
// ✅ Good — actual order ID from your backend
transaction_id: 'ORD-2024-00847'

The transaction_id is your primary key for deduplication and for reconciling GA4 data against your order system. It must be the real order ID from your backend — not a timestamp, not a random string, and definitely not a static value.