This is the ecommerce dataLayer specification I hand to developers. It follows GA4's recommended events exactly, so GA4's reports work without configuration, and it carries everything Google Ads and Meta tags need. Implement it once and every platform reads from the same source.
Global Rules
- Push
window.dataLayer = window.dataLayer || [];before any push. - Push
{ ecommerce: null }immediately before every ecommerce event push. - All monetary values are plain numbers in major units, for example
59.95, never strings and never cents. currencyis the three-letter ISO code of the store currency used for the values.- Every event in this specification fires exactly once per occurrence.
- Item objects follow the schema below everywhere; the same product has the same
item_idin every event.
The Item Object
| Parameter | Type | Required | Notes |
|---|---|---|---|
| item_id | string | Yes | SKU or variant ID |
| item_name | string | Yes | Product name |
| price | number | Yes | Unit price after item-level discount |
| quantity | number | Yes | Integer |
| item_brand | string | Recommended | |
| item_category | string | Recommended | Broadest category |
| item_category2 to item_category5 | string | Optional | Narrower levels |
| item_variant | string | Recommended | Size, colour |
| discount | number | Optional | Unit discount applied |
| coupon | string | Optional | Item-level coupon |
| index | number | Optional | Position in a list |
| item_list_id, item_list_name | string | Optional | Where it was selected from |
| affiliation | string | Optional | Store or seller |
Event Specification
view_item_list
Fires when a list of products renders, once per list. Include up to the first 20 items.
dataLayer.push({ ecommerce: null });
dataLayer.push({
event: 'view_item_list',
ecommerce: {
item_list_id: 'category_cushions',
item_list_name: 'Cushions',
items: [{ item_id: 'SKU-401', item_name: 'Lumbar Support Cushion', price: 59.95, quantity: 1, index: 0, item_list_id: 'category_cushions', item_list_name: 'Cushions' }]
}
});
select_item
Fires when a product is clicked in a list.
dataLayer.push({ ecommerce: null });
dataLayer.push({
event: 'select_item',
ecommerce: {
item_list_id: 'category_cushions',
item_list_name: 'Cushions',
items: [{ item_id: 'SKU-401', item_name: 'Lumbar Support Cushion', price: 59.95, quantity: 1, index: 0 }]
}
});
view_item
Fires on the product page, once per load, with the selected or default variant.
dataLayer.push({ ecommerce: null });
dataLayer.push({
event: 'view_item',
ecommerce: {
currency: 'AUD',
value: 59.95,
items: [{ item_id: 'SKU-401', item_name: 'Lumbar Support Cushion', item_brand: 'Posture Co', item_category: 'Cushions', item_variant: 'Grey', price: 59.95, quantity: 1 }]
}
});
add_to_cart and remove_from_cart
Fire on confirmed cart API success. value is price times quantity for the items changed, not the cart total.
dataLayer.push({ ecommerce: null });
dataLayer.push({
event: 'add_to_cart',
ecommerce: {
currency: 'AUD',
value: 119.90,
items: [{ item_id: 'SKU-401', item_name: 'Lumbar Support Cushion', item_variant: 'Grey', price: 59.95, quantity: 2 }]
}
});
view_cart
Fires on the cart page with all items and the cart value.
begin_checkout
Fires when checkout starts, with all items, value and coupon if applied.
add_shipping_info and add_payment_info
Fire when the respective step completes, with all items, value, and shipping_tier or payment_type.
purchase
Fires on the order confirmation page, once per order, guarded by the order ID.
dataLayer.push({ ecommerce: null });
dataLayer.push({
event: 'purchase',
ecommerce: {
transaction_id: '10234',
value: 129.90,
tax: 11.81,
shipping: 9.95,
currency: 'AUD',
coupon: 'WELCOME10',
items: [{ item_id: 'SKU-401', item_name: 'Lumbar Support Cushion', item_brand: 'Posture Co', item_category: 'Cushions', item_variant: 'Grey', price: 59.95, quantity: 2 }]
},
user_email: 'name@example.com',
user_phone: '+61400000000',
customer_type: 'new'
});
value is the order total excluding tax and shipping unless the business decides otherwise, and the decision must be applied everywhere. user_email and user_phone feed Enhanced Conversions and Advanced Matching; omit them if the business prefers.
refund
Fires when a refund is processed, usually server-side or from an admin action; for full refunds send transaction_id and value; for partial refunds include the refunded items.
view_promotion and select_promotion
Fire when a banner is visible or clicked, with promotion_id, promotion_name, creative_name, creative_slot and the items it promotes if applicable.
Deduplication Guard for purchase
(function () {
var orderId = '10234';
try {
if (localStorage.getItem('purchase_' + orderId)) { return; }
localStorage.setItem('purchase_' + orderId, '1');
} catch (e) {}
// purchase push here
})();
On server-rendered platforms, also mark the order as tracked server-side so the push is not output on reload.
How Tags Read It
- GA4 Event tags with the ecommerce data option read
ecommercedirectly. - Google Ads Conversion Tracking reads
ecommerce.value,ecommerce.currency,ecommerce.transaction_idthrough Data Layer Variables. - Meta Purchase reads
ecommerce.value,ecommerce.currency, and a Custom JavaScript variable that mapsecommerce.itemstocontent_idsand sumsquantityfornum_items.
Validation Checklist for the Developer
- Every push has
eventand a completeecommerceobject. { ecommerce: null }precedes every ecommerce push.- Numbers are numbers; no currency symbols, no strings, no cents.
item_idis stable and matches the catalogue feed used in Google Merchant Center and Meta Catalog.purchasefires once per order, survives refresh, and matches the order total in the platform.add_to_cartfires on API success, not on button click.- In GTM Preview, the Data Layer tab shows exactly the structure above after each event.
- In GA4 DebugView, each event shows the items array expanded.
Outcome
One specification, one implementation, and GA4, Google Ads and Meta all reporting the same products, values and orders. When a number disagrees between platforms, you check attribution settings, not the data.
