If your Shopify store used to send clean purchases to GA4 and now sends nothing, or sends them twice, check where the purchase event is collected. This guide covers Shopify’s current event path, the retired script locations, and the limits of a custom pixel.
How does Shopify fire the purchase event 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.
What happened to Additional Scripts and checkout.liquid?
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 Thank you and Order status pages was a legacy tracking location. August 28, 2025 was the Plus sunset. For non-Plus stores, Shopify made Additional Scripts view-only on that date, then required the page upgrade by August 26, 2026. Order-status script tags had the same Plus/non-Plus retirement dates. Check the store’s page version and order data rather than assuming one date describes every store.
The practical takeaway: if a store still lists its purchase tag in Additional Scripts, audit whether that tag actually runs and whether another integration sends the purchase. If it no longer runs, migrate the missing event to a supported pixel and verify it with a test order and order reconciliation.
The limits of a custom pixel implementation
Google does not support Google tags inside Shopify custom pixels and recommends the Google & YouTube app. A sandboxed tag can send data while other features fail. I check the supported app before choosing a custom implementation.
The following is a hypothetical custom-pixel example, not a supported Google setup or a tested production implementation. It shows the event mapping for an upgraded checkout with line-level discounts. Order-level discounts and tax-inclusive prices need separate checks against the order; this example does not establish those adjustments. The sandbox loads gtag itself rather than using the theme’s tag.
// 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;
if (!checkout.order?.id || !checkout.currencyCode || !checkout.lineItems?.length) return;
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.finalLinePrice.amount / line.quantity,
quantity: line.quantity,
}));
gtag('event', 'purchase', {
transaction_id: checkout.order.id,
value: items.reduce((sum, item) => sum + item.price * item.quantity, 0),
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.
What does the GA4 items array require?
Google lists items and transaction_id as required purchase parameters. An event arriving without them does not establish that the ecommerce implementation meets the documented schema.
Per Google’s ecommerce reference, each item needs at least one of item_id or item_name; sending both makes it easier to inspect. price, quantity, and item_variant make the item reports usable. A few rules that save debugging time:
valueis the sum of itemprice × quantity, excluding tax and shipping. Use discounted unit prices and send tax and shipping in their separate parameters. Reconcile the same value definition against Shopify.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.
How does GA4 deduplicate Shopify purchases?
GA4 deduplicates purchases with the same transaction_id in web streams. Google describes this for transactions from the same user; it is not a guarantee across different user contexts or app streams. Two rules matter:
- Fire the purchase from exactly one path. Shopify’s native GA4 sales channel and a custom pixel can both send the order in different user contexts. A matching transaction ID alone does not prove deduplication worked. Choose one path and verify the received event.
- 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, Shop Pay, express or wallet checkout, and post-purchase upsells. Shopify emits
checkout_completedonce: usually on Thank you, but on the first upsell offer page for post-purchase flows. If that page does not load, the event is not emitted. - Reconcile after processing. Compare GA4 purchases against Shopify orders by transaction ID after the reports have processed. Google says processing can take 24–48 hours. Reconciliation measures recorded coverage; it cannot prove browser collection captured every order regardless of consent or a failed page load.
Common failure modes
- Double pixels. The native GA4 sales channel and a custom pixel can send duplicate purchases. Keep one purchase path and check IDs and user context before concluding that revenue doubled.
- 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 is configured in a retired script location. Check the store’s page version and whether a replacement pixel or app already sends the event before changing the setup.
- 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 inherits the limits of client-side collection and is not Google’s supported Shopify integration. Its configured privacy requirements can block it while consent is denied. A failed page load or browser blocking can also prevent collection.
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 Google & YouTube app is Google’s supported setup. Check for an existing custom purchase tag before adding it, so that two paths do not send the same order.
- Custom pixel with GTM or gtag (the hypothetical example above). You own the code, but Google does not support this setup. Sending one event successfully does not prove other Google tag features work.
- Server-side. A server-side setup can forward Shopify order webhooks while browser events supply session and consent context. Webhooks do not contain browser cookies. The setup needs hosting, monitoring and a consent design; it does not remove the consent requirement.
If you want the purchase path chosen, verified and reconciled for you rather than maintaining it yourself, that is the scope of my GA4 implementation work; the verification method behind it is order reconciliation.