- Five events, in order, once each.
view_cart,begin_checkout,add_shipping_info,add_payment_info,purchase, each withcurrency,valueand anitemsarray.purchasealso needs a uniquetransaction_id. - The middle of the funnel is where implementations fail. Most stores have
purchaseworking because revenue is what everyone checks.add_shipping_infois the step most often missing entirely. - Express wallets distort everything. Apple Pay, Google Pay and Shop Pay skip shipping and payment entry, so their buyers vanish between
begin_checkoutandpurchase. Segment them out before you diagnose a drop. - Clear the ecommerce object before every push. Without
ecommerce: null, the previous event's items leak into the next one and the funnel becomes subtly wrong rather than obviously broken. - The direction of the GA4 vs processor gap tells you the cause. Higher means duplication. Lower means events are not arriving.
- Consent mode removes part of the funnel. Below Google's published thresholds, denied EU and UK traffic is simply absent, not modelled.
A complete GA4 checkout funnel is five events fired in order: view_cart, begin_checkout, add_shipping_info, add_payment_info, purchase. Each carries a currency, a value, and an items array; purchase additionally needs a unique transaction_id. Implement those five correctly and you can see which step of your checkout is losing money. Implement four of them, or fire one at the wrong moment, and you get a funnel that looks plausible and points at the wrong problem.
That second outcome is the common one. Most stores have purchase working, because revenue is the thing everyone checks. The middle of the funnel, the part that would actually tell you where shoppers give up, is where implementations quietly fall apart, and the failures are specific and predictable. This guide covers the full event chain, what breaks at each step, and how to tell the difference between a checkout problem and a tracking problem.
What is the GA4 checkout funnel?
GA4 replaced Universal Analytics' numbered checkout steps with a fixed set of recommended ecommerce events. There are no configurable step numbers any more. You build the funnel from named events, and the names are not negotiable if you want GA4's built-in reports to work.
| Event | Fire when | Required parameters | Notable optional parameters |
|---|---|---|---|
view_cart |
Shopper views the cart | currency, value, items |
coupon |
begin_checkout |
Shopper enters the checkout flow | currency, value, items |
coupon |
add_shipping_info |
Shopper selects a delivery option | currency, value, items |
shipping_tier, coupon |
add_payment_info |
Shopper submits payment details | currency, value, items |
payment_type, coupon |
purchase |
Order is completed | transaction_id, currency, value, items |
tax, shipping, coupon |
refund |
Order is refunded | transaction_id, currency |
value, items, tax, shipping |
Two rules from Google's documentation are worth stating plainly, because breaking them is the single most common cause of a funnel that looks empty.
Set currency at the event level whenever you send value. Without it, the revenue data has nowhere to go in standard reports.
Populate every parameter you have data for, even the optional ones. shipping_tier and payment_type cost nothing to add and are the difference between "people drop at the shipping step" and "people drop at the shipping step when the only option is a €12 courier". Each item can carry the prescribed fields plus up to 27 custom parameters, and the items array can hold up to 200 elements.
How do I set up begin_checkout in GA4?
If you are using Google Tag Manager, which you should be for anything beyond a trivial store, you push a structured object into the data layer and GTM reads it.
// Clear the previous ecommerce object first.
// Without this, parameters from the last push leak into this one.
dataLayer.push({ ecommerce: null });
dataLayer.push({
event: "begin_checkout",
ecommerce: {
currency: "EUR",
value: 89.97, // sum of price x quantity across items
coupon: "SPRING10",
items: [
{
item_id: "SKU_12345",
item_name: "Merino Crew Sweater",
item_brand: "Example",
item_category: "Knitwear",
item_variant: "Charcoal / M",
price: 29.99,
quantity: 3,
index: 0
}
]
}
});
Then in GTM: a Custom Event trigger matching begin_checkout, firing a GA4 Event tag with the event name begin_checkout and the ecommerce data loaded from the data layer.
The ecommerce: null push matters more than it looks. The data layer persists, so without clearing it, an items array or a coupon from an earlier event can attach itself to the next one. This produces funnels that are subtly wrong rather than obviously broken, which is the worst kind.
The other four events follow the same shape. add_shipping_info adds shipping_tier, add_payment_info adds payment_type, and purchase adds transaction_id, tax and shipping:
dataLayer.push({ ecommerce: null });
dataLayer.push({
event: "purchase",
ecommerce: {
transaction_id: "ORD-100472", // your real order number, never a timestamp
currency: "EUR",
value: 89.97,
tax: 16.80,
shipping: 4.95,
coupon: "SPRING10",
items: [ /* same shape as begin_checkout */ ]
}
});
What breaks at each step?
This is the part no implementation guide covers, and it is where the actual work is. Each event in the chain has its own characteristic failure.
view_cart
The mildest problems live here, but two are common. Stores with a slide-out cart drawer often fire view_cart on every drawer open, including the one that happens automatically after adding an item, so view_cart outnumbers add_to_cart, which is nonsense. And stores that only fire it on a dedicated /cart page miss every shopper who goes straight from the drawer to checkout, making the first funnel step look narrower than reality.
Decide which interaction counts as viewing the cart, fire it once per interaction, and be consistent.
begin_checkout
The most misplaced event in ecommerce analytics. The question that decides everything: does it fire when the shopper clicks the checkout button, or when the checkout page loads? Those are different populations, and the gap between them (people who clicked but never arrived because the page was slow, errored, or redirected badly) is exactly the kind of loss you want visible. Fire on page load, or on entry to the checkout view in a single-page app, and you will never see it.
The second issue is re-firing. A shopper who leaves checkout to change a quantity and returns will trigger begin_checkout again. That is acceptable, since GA4 funnels count users rather than events, but it means raw event counts will overstate unique checkout entries. Read the funnel exploration, not the event count, when you want a completion rate.
The third is platform-dependent and genuinely nasty. On Shopify, the underlying checkout_started event behaves differently depending on your checkout: with Checkout Extensibility enabled it fires every time a customer enters checkout, but on non-extensible shops it only fires the first time. A store that migrated between the two will see a step change in its funnel that has nothing to do with shopper behaviour.
add_shipping_info
The step most often missing entirely, for a structural reason: on many checkouts there is no clean moment that means "the shopper chose a delivery option".
If shipping methods are pre-selected by default, the shopper may never actively choose anything, so there is no interaction to bind a trigger to. If shipping options render conditionally after an address is entered, the event can fire before the options exist. And on a one-page checkout there are no page transitions at all, so the natural trigger boundary that a multi-step flow gives you for free simply is not there. That loss of funnel granularity is one of the trade-offs we cover in our comparison of one-page and multi-step checkout.
The workable pattern is to fire it on the first meaningful commitment to a shipping choice (a radio-button change, or the blur event when the shopper moves from the shipping block to the payment block) and then guard it so it fires once per checkout session, not once per re-render. Framework-driven checkouts re-render constantly; an unguarded trigger will fire dozens of times.
If you cannot instrument this step honestly, leave it out rather than firing it on page load. A step that fires for 100% of people who reach it tells you nothing and quietly makes the next step look like the problem.
add_payment_info
Three distinct failure modes, and most stores have at least one.
Payment fields inside an iframe. Stripe Elements, Adyen components, hosted card fields from your payment service provider: your page cannot see keystrokes or submit events inside a cross-origin iframe. You need the payment SDK's own callback (change, complete, or the equivalent) to know the shopper filled it in.
Redirect gateways. If the shopper leaves for a hosted payment page, they may never return to a page of yours that could fire this event. The order still completes via webhook; the funnel step never does.
Express wallets. This is the one that sends teams chasing ghosts. A shopper paying with Apple Pay, Google Pay or Shop Pay skips shipping selection and payment entry as your checkout understands them, because the wallet supplies both. So add_shipping_info and add_payment_info never fire, and purchase does. If express wallets are a meaningful share of your orders, your funnel will show a large, alarming drop between begin_checkout and add_payment_info that represents your best-converting customers. Segment wallet checkouts out before you diagnose anything, or you will optimise a step that is not broken.
purchase
The event everyone has, and the one most likely to be double-counted.
The recurring causes are well documented across analytics practitioners: the confirmation page being reloaded or reached via the Back button, a GTM trigger without a once-per-page constraint, two tracking implementations running in parallel (a platform app plus a custom container, or a leftover tag from a previous migration), the payment gateway re-firing after a redirect, and browser retries on a slow connection. Server-side tagging adds another: the browser and the server each sending the same purchase.
GA4 does collapse purchase events arriving with an identical transaction_id close together, and that catches some of it. Do not treat it as a solution. Practitioners consistently report that the window is limited (a shopper who reloads the confirmation page the next day can still be counted twice), and it only works if transaction_id is actually present and actually stable. A blank ID, or one generated from a timestamp that changes on every page load, passes straight through. Use your real order number, fix the duplicate source, and keep transaction_id as the backstop rather than the mechanism.
Also: send a refund event with the matching transaction_id. Otherwise your GA4 revenue diverges from your books by the value of every return, permanently.
Why don't my GA4 numbers match my payment processor?
They never will exactly. The question is how far apart is acceptable, and which direction the gap runs, because the direction tells you the cause.
GA4 higher than the backend almost always means duplication. Work through the list above, and note that the shape of the discrepancy narrows it down: every order doubled points to two tags firing; some orders doubled and others clean points to confirmation-page reloads; a jump immediately after the payment redirect points to the gateway.
GA4 lower than the backend means events are not arriving. Ad blockers and tracking prevention account for a meaningful share. Redirect gateways where the shopper closes the tab before returning account for more. Orders placed by phone, in-store or through a marketplace never touch your site at all. And asynchronous payment methods (bank transfer, some buy-now-pay-later providers, invoice terms) confirm minutes or days after the session ended, so a browser-based tag cannot fire. The fix for that last category is the Measurement Protocol, firing purchase from your backend when the payment actually settles.
| What you see | Likely cause | What to do |
|---|---|---|
| GA4 higher, every order doubled | Two implementations firing: a platform app plus a custom container, or a leftover tag from a migration | Find and remove the second implementation. Check what is actually connected before adding anything |
| GA4 higher, some orders doubled | Confirmation page reloaded or reached via Back; GTM trigger with no once-per-page constraint | Constrain the trigger to once per page and fix the reload path. Keep transaction_id as the backstop, not the fix |
| GA4 higher, jump right after the payment redirect | Gateway re-firing on return; browser and server-side tagging both sending the same purchase | Send the purchase from one place, or deduplicate on the server by the real order number |
| GA4 lower, spread evenly across orders | Ad blockers and browser tracking prevention | Treat as a known, stable share. Server-side tagging recovers some of it |
| GA4 lower, concentrated on redirect payment methods | Shopper closed the tab before returning from the hosted payment page | Fire purchase from the backend via the Measurement Protocol when the webhook confirms |
| GA4 lower, orders confirmed hours or days later | Bank transfer, invoice terms, some BNPL: the payment settles after the session ended | Measurement Protocol on settlement, never a browser tag |
| GA4 lower, orders that never touched the site | Phone, in-store and marketplace orders in the same backend | Exclude them from the reconciliation before comparing anything |
A small single-digit gap is normal. A gap that is large, or that changes suddenly, is a measurement bug, and you should find it before you trust any funnel built on top of it. Reconcile GA4 transactions against real orders for the same dates and the same timezone before you start, not after. The same check opens our diagnostic for a post-redesign conversion drop, for the same reason: a number you have not reconciled cannot tell you what moved.
What does consent mode do to the funnel?
If you sell into the EU or UK, a share of your traffic is generating no identifiable data at all, and the way GA4 compensates for it is easy to misread.
Under advanced consent mode, tags still fire when analytics consent is denied, but send anonymous pings without identifiers. GA4 can then apply behavioural modelling to estimate what those users did, but only above volume thresholds that Google publishes: at least 1,000 events per day with analytics_storage='denied' for 7 days, and at least 1,000 daily users sending events with consent granted on 7 of the previous 28 days. Below those, no modelling happens and the data is simply missing. Plenty of mid-market stores never reach the threshold.
There is a subtlety that matters specifically for funnels. Google's documentation notes that modelling is applied differently in path and funnel explorations than in standard reports, and that unconsented events become reportable but are not themselves modelled. In practice: your funnel exploration and your events report can disagree, both can be right, and neither is a raw count. When you report a checkout completion rate to a stakeholder, say which one you used.
Basic consent mode, where tags are blocked entirely until consent is given, models nothing at all. If your cookie banner uses a reject-by-default pattern and your consent rate is low, a large slice of your funnel does not exist, and it is not randomly distributed, since the people who reject tracking are not a random sample of your buyers.
What's different on Shopify and Magento?
Shopify. Since checkout extensibility, your theme's JavaScript does not run in checkout at all. Checkout events are only reachable through Shopify's Web Pixels system, as a fixed catalogue of standard events. Two of the ones you need, checkout_contact_info_submitted and checkout_shipping_info_submitted, are documented as available only where Checkout Extensibility is enabled. The built-in Google & YouTube channel covers the standard funnel with server-confirmed data and is the right answer for most stores; a custom pixel gives you more control but runs in a sandbox with real constraints, and merchants have long reported reliability problems when pulling from checkout.order. Whichever you use, check Settings, then Customer events, first to see what is actually connected. A large share of "my events aren't firing" cases are two implementations running at once. Our Shopify checkout optimisation service covers what each plan can and cannot instrument.
Magento 2 / Adobe Commerce. There is no complete GA4 data layer out of the box; most stores use a GTM extension that builds it. Two platform-level details are worth knowing. Adobe Commerce 2.4.8 (April 2025) fixed the GTM add-to-cart data layer for configurable products, so if your configurable-product data has looked wrong for years on an older patch level, the fix may be a core upgrade rather than anything in your container. The same release also allowed connections to region1.analytics.google.com, a Content Security Policy change that had been throwing errors for EU visitors. If you inherited a GTM container built against the old Enhanced Ecommerce format, assume it needs rebuilding rather than mapping: the schemas are genuinely different, not just renamed. Our Magento abandonment guide covers the silent JavaScript errors and slow totals calls that sit next to these in the same checkout.
How do I build the funnel report once events are firing?
In GA4, use Explore, then Funnel exploration, rather than the standard reports. Add your five events as open funnel steps in order, then do the three things most teams skip.
Segment by device. Mobile and desktop checkout completion rates are usually far enough apart that an aggregate number describes neither.
Segment by market. Payment method availability, shipping cost and consent rates all vary by country. A single blended funnel hides the one market that is broken.
Split out express-wallet checkouts. As above. Otherwise they permanently distort the middle of your funnel.
Then apply the same judgment you would apply to any measurement: a step that shows a 0% drop-off is broken, not perfect, and a step that shows 100% drop-off is almost always a missing event rather than a catastrophic UX failure.
A validation protocol you can run in an afternoon
- Enable GA4 DebugView and GTM Preview.
- Place a complete test order. Count every event and note its exact trigger moment.
- Confirm each event fires once, with
currency,valueand a populateditemsarray. - Reload the confirmation page. If a second
purchaseappears, you have found your duplication. - Repeat on mobile, on a throttled connection.
- Repeat with an express wallet, and with each payment method that redirects.
- Repeat in each market you sell into. Payment and shipping options differ, and so do the triggers bound to them.
- Reconcile a full week of GA4 transactions against real orders, same dates, same timezone.
- Check your consent banner's actual rate, and whether you are on basic or advanced consent mode.
If steps 6 and 7 surface problems, that is the normal outcome, not a sign that something unusual is wrong. Those are the paths nobody tests. The analytics points in our 77-point checkout audit checklist are the same checks in list form, alongside the rest of the checkout.
Not there yet? Our free automated site audit covers everything that happens before checkout (clarity, trust signals, SEO, performance and security) and walks a guest checkout as far as it can go without placing an order. What it cannot see is which events fire along the way or how they compare to your orders, and the report says so. That boundary is the honest one.
Sources
The event schema, parameter limits, consent thresholds and platform behaviour above are drawn from the following primary sources.
- Google Analytics: Measure ecommerce (event schema and parameters)
- Google Analytics Help: Behavioral modeling for consent mode
- Shopify: Web Pixels customer events reference
- Adobe Commerce: 2.4.8 release notes
Event names, parameters and thresholds in this article follow the primary documentation listed above, which changes; check the current spec before implementing. Coverage statistics we could not trace to a methodology have been left out. Last reviewed October 2026.