Fix the GA4 Purchase Event on Shopify (Step by Step)
How the Shopify purchase event fires today, why Additional Scripts is dying, and a correct custom pixel that sends a GA4 purchase with transaction_id.
·
Shopify fires the purchase from Customer Events (web pixels), which run in a sandbox isolated from your theme, so a snippet in Additional Scripts no longer reaches GA4. The fix is a custom pixel that subscribes to checkout_completed and sends a GA4 purchase event with a unique transaction_id, value, currency, and items. Run exactly one purchase path, verify it in DebugView, then reconcile against Shopify the next day.
Key takeaways
- Shopify fires the purchase through Customer Events (web pixels) in a sandbox — code in Additional Scripts or checkout.liquid can no longer send it to GA4.
- Shopify has announced that non-Plus stores are auto-upgraded off Additional Scripts on August 26, 2026, so any purchase tag still living there will stop firing.
- A correct custom pixel subscribes to checkout_completed and sends a GA4 purchase with a unique transaction_id, value, currency, and a properly built items array.
- Deduplication depends on a consistent transaction_id and, above all, on firing the purchase from exactly one path — never the native GA4 channel and a custom pixel at once.
- Verify within the sandbox's limits: DebugView and Realtime for the live event, then next-day order reconciliation for coverage.
If your Shopify store used to send clean purchases to GA4 and now sends nothing, or sends them twice, the cause is almost always the same: where the purchase event is allowed to live has changed. This guide covers how Shopify fires the purchase today, which old methods are being switched off, and the custom pixel that does it correctly.
The short answer
On modern Shopify checkout, the purchase event is fired from Customer Events — Shopify’s web pixels — which run in a sandbox that is isolated from your theme. That means the old approaches, a snippet in Additional Scripts or an edit to checkout.liquid, can no longer reach GA4. The correct fix is a custom pixel that subscribes to the checkout_completed event and sends a GA4 purchase with a unique transaction_id, a value, a currency, and a properly built items array. Then you make sure that purchase fires from exactly one path, and you verify it.
How Shopify fires the purchase today
Shopify moved checkout into an extensible model, and tracking moved with it. Instead of injecting scripts into checkout pages, you register a pixel that subscribes to the events Shopify emits — page views, product views, add-to-cart, and, for the completed order, checkout_completed.
The important architectural fact is the sandbox. Custom pixels do not run on your storefront page. They run in a separate, restricted context with their own window, isolated from your theme’s scripts. This is deliberate: it stops third-party pixels from reading everything on the page. But it has two consequences that trip up almost every broken setup I see:
- A GTM container or
gtagsnippet loaded by your theme cannot see checkout events, because the pixel and the theme live in different contexts. - Anything you want to send from the checkout — your GA4 tag included — must be loaded inside the pixel itself, or forwarded from Shopify to a server.
Once you internalize that, the old symptoms make sense: the reason your theme’s GTM stopped catching purchases is not a bug, it is the sandbox doing its job.
The legacy paths that are going away
Two older methods still appear in guides and inherited stores, and both are dead ends.
checkout.liquidcustomizations were how Plus stores injected tracking into the checkout. That path has been retired in favour of checkout extensibility.- Additional Scripts on the order status and thank-you pages is the one most non-Plus stores relied on. Shopify has made that field view-only and has announced that non-Plus stores on Basic, Shopify, and Advanced plans are auto-upgraded off it on August 26, 2026. After that auto-upgrade, per Shopify’s announcement, any script still living in Additional Scripts — conversion tags, pixels, custom snippets — stops firing. If your GA4 purchase is still there, it will simply go silent, and your revenue in GA4 will fall off a cliff on that date.
The practical takeaway: if a store’s purchase event is still in Additional Scripts, migrating it to a custom pixel is not optional maintenance, it is a hard deadline.
The correct custom pixel implementation
Here is a technically correct custom pixel that subscribes to checkout_completed and sends a GA4 purchase. Because the pixel runs in the sandbox, it loads gtag itself rather than assuming your theme’s tag is reachable.
// Shopify admin → Settings → Customer events → Add custom pixel
// This code runs in Shopify's pixel sandbox: it has its own window,
// isolated from your theme. A GTM/gtag snippet on the theme is NOT
// reachable here, so load gtag inside the pixel and fire from here.
const MEASUREMENT_ID = 'G-XXXXXXXXXX';
// 1) Load gtag.js into the sandbox and configure GA4
const s = document.createElement('script');
s.src = 'https://www.googletagmanager.com/gtag/js?id=' + MEASUREMENT_ID;
s.async = true;
document.head.appendChild(s);
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', MEASUREMENT_ID, { send_page_view: false });
// 2) Fire the purchase exactly once, when the checkout completes
analytics.subscribe('checkout_completed', (event) => {
const checkout = event.data.checkout;
const items = (checkout.lineItems || []).map((line) => ({
item_id: line.variant && (line.variant.sku || line.variant.id),
item_name: line.title,
item_variant: line.variant && line.variant.title,
price: line.variant && line.variant.price && line.variant.price.amount,
quantity: line.quantity,
}));
gtag('event', 'purchase', {
transaction_id: (checkout.order && checkout.order.id) || checkout.token,
value: checkout.totalPrice && checkout.totalPrice.amount,
currency: checkout.currencyCode,
tax: checkout.totalTax && checkout.totalTax.amount,
shipping:
checkout.shippingLine &&
checkout.shippingLine.price &&
checkout.shippingLine.price.amount,
items: items,
});
});
If you route through Google Tag Manager instead, the same analytics.subscribe block pushes an equivalent object to a dataLayer that a container loaded inside the pixel reads. The rule does not change: the destination tag has to live in the sandbox with the subscription, not on the theme. For the mechanics of that object, see the note on the dataLayer.
The items array requirements
GA4 will accept a purchase with no items, and it will look fine in the revenue total — then every product, category, and item report is empty. So the items array is not optional in practice. Per Google’s ecommerce reference, each element should carry at least an item_id and an item_name; price, quantity, and item_variant make the item reports usable. A few rules that save debugging time:
valueshould equal what the customer paid for the order and, as a habit, reconcile with the sum of item prices and quantities plus tax and shipping.currencyis required and must be a three-letter ISO code. In multi-currency stores, send the currency the customer actually transacted in, and make surevalueis in that same currency.item_idmust be stable. Use the SKU or variant ID consistently, because that is the key GA4 uses to join items across events.
Deduplication with transaction_id
transaction_id is the single most important parameter for revenue accuracy. GA4 uses it to deduplicate purchases: it deduplicates purchase events that share the same transaction ID. That gives you a safety net, but it is a net with holes, and you should not lean on it as your primary defence. Two rules matter more:
- Fire the purchase from exactly one path. The classic double-count is Shopify’s native GA4 sales channel and a custom pixel both sending the order. Even with a correct transaction ID, that is the setup most likely to inflate revenue. Choose one path and turn the other off.
- Never send an empty transaction_id. GA4 deduplicates all purchases with an empty transaction ID together, so a blank value does not just fail to help — it actively collapses genuine orders into one. Always send the real order ID.
If you have already seen GA4 running higher than Shopify, deduplication is where to look first. The companion article on why GA4 revenue does not match Shopify walks through catching those duplicates in reconciliation.
Verification protocol
Verifying inside the sandbox is different from verifying a normal page, and the usual first instinct — open GTM Preview or Tag Assistant — largely does not work here. This is the sequence I trust.
- DebugView, on a real order. Put a test device into debug mode and complete an actual checkout. Watch the
purchaseland in DebugView and inspect its parameters: istransaction_idpresent and non-empty, isvalueright, iscurrencycorrect, doesitemscontain the products? - Realtime as a sanity check. The purchase should appear in the Realtime report within moments. Realtime confirms the event is arriving even when DebugView attachment is fiddly.
- Accept the tooling limits. Tag Assistant and GTM Preview cannot hook into the pixel sandbox the way they hook into a page. Not seeing the event there is expected; it is not evidence the pixel failed. Judge success by DebugView and Realtime.
- Every payment path, not one. Test standard card, then Shop Pay and any express or wallet checkout, then any post-purchase upsell.
checkout_completedshould fire for all of them; the whole point of migrating is coverage. - Next-day reconciliation. The morning after, compare a day of GA4 purchases against Shopify orders by transaction ID. Live testing proves the event can fire; reconciliation proves it fired for every order. That is the difference between “it works on my device” and “it works.”
Common failure modes
- Double pixels. The native GA4 sales channel and a custom pixel both fire, doubling revenue. Keep one.
- Missing express paths. The purchase is bound to a flow that Shop Pay or express checkout skips, so wallet orders never send a purchase and go missing during reconciliation. This is the same class of fault covered in Shopify checkout tracking broken.
- Consent blocking. Under a consent banner, analytics storage may be denied, so the pixel is not permitted to send for those visitors. That is a legitimate loss, not a bug to code around. This covers technical implementation, not legal advice — for the mechanism, see consent mode v2.
- Stale Additional Scripts tag. The purchase still lives in the deprecated field and is running on borrowed time until the auto-upgrade switches it off.
- Empty or mismatched transaction_id. A blank ID collapses orders; an ID that differs between paths defeats deduplication entirely. See the related breakdown of GA4 not tracking purchases.
Limitations
A custom pixel is the correct client-side fix, but it inherits the limits of client-side collection. It cannot send for consent-denied visitors, and it will still lose orders to aggressive browser tracking prevention and to storage being cleared between sessions. The sandbox also constrains debugging, so verification leans on DebugView, Realtime, and reconciliation rather than the usual page-level inspectors. And a pixel fixes the purchase; it does not, on its own, repair upstream events like add-to-cart if those were broken by the same checkout migration.
Alternatives
There are three broad ways to send the Shopify purchase, and they are not mutually exclusive.
- Native Shopify integration. The built-in Google channel is the least effort and fine for a basic setup, but you get little control over parameters and it is the usual culprit in double-counting when a custom pixel is added alongside it.
- Custom pixel with GTM or gtag (this guide). More control, correct for the sandbox, and the right home for a tuned
itemsarray — at the cost of maintaining the code. - Server-side. Forwarding the order from Shopify to a server-side setup recovers some of what browsers block and moves collection off the client entirely. It is the most robust and the most involved, and it does not remove the consent requirement.
If you want the pixel built, verified, and reconciled for you rather than maintaining it yourself, that is exactly the scope of my GA4 implementation work; the verification method behind it is order reconciliation.
Sources
FAQ
Do I still need the native GA4 sales channel if I use a custom pixel?
No — pick one. If Shopify's native Google integration and your custom pixel both send the purchase, GA4 receives it twice and revenue doubles. Keep the native integration or the custom pixel, never both. When I want full control over the parameters, I run the custom pixel and disable the native purchase path.
Can I use my theme's GTM container to fire the Shopify purchase?
Not for the new checkout. A GTM container loaded on your theme cannot see checkout or thank-you events, because those run in a separate sandbox. You either load gtag or GTM inside the custom pixel itself, or send the purchase server-side. A theme-level container alone will miss the purchase.
Why doesn't Tag Assistant or GTM Preview show my purchase event?
The custom pixel runs in a sandboxed context that Tag Assistant and GTM Preview cannot attach to the way they attach to a normal page. That is a limitation of the tools, not proof the event failed. Verify with GA4 DebugView and Realtime instead, then confirm coverage with next-day reconciliation.
The purchase fires on card checkout but not on Shop Pay or express checkout. Why?
The checkout_completed event is meant to fire for any completed order, including wallet and express paths. If it fires on card checkout but not on those, the pixel usually is not subscribed correctly, or an app is intercepting the flow. Test every payment path individually rather than assuming one covers the rest.
What should I use as the transaction_id?
Use the Shopify order ID or order name, the same value on every path, unique per order. Never send an empty string: GA4 deduplicates purchases with an empty transaction_id together, which silently collapses your revenue. A consistent, non-empty ID is what makes deduplication work.
Keep reading
Not sure whether your tracking is actually broken?
Start with a Health Check — a fast, read-only diagnosis that tells you what is wrong before you spend anything fixing it. Prefer email? Send your store URL and one sentence about what looks off.