Tracking & Attribution

How Do You Move a GTM dataLayer Into Shopify's New Checkout?

checkout.liquid is gone, so your GTM dataLayer pushes are too. Here's how to rebuild them from Shopify's checkout events, and when to skip GTM entirely.

Quick answer

You don't move it; you rebuild it. The upgraded checkout doesn't run checkout.liquid, so the old dataLayer pushes are gone. Instead, a custom pixel subscribes to Shopify's standard checkout events (checkout_started, the contact, address, shipping and payment steps, and checkout_completed), rebuilds each push from the event's checkout data, and loads GTM inside the pixel sandbox. If GTM's only job was sending conversions to ad platforms, send those server-side and drop GTM from checkout.

Tell us what's broken. We'll fix your tracking — free.

Describe the tracking/attribution problem you're stuck on and we'll map it to a fix: server-side conversions to Meta, Google, TikTok and Pinterest, plus first-party tracking that survives Safari. No code required.

You don’t move the dataLayer; you rebuild it. Shopify’s upgraded checkout no longer renders checkout.liquid, so every dataLayer.push() you wrote there has stopped running. The replacement is a custom pixel that listens for Shopify’s own checkout events, rebuilds each push from the event data, and loads Google Tag Manager (GTM) inside the pixel’s sandbox.

That works, with limits. And if GTM in checkout only existed to send conversions to Meta, Google or TikTok, there’s a simpler route: send those events server-side and keep GTM out of checkout entirely.

Why did your checkout dataLayer stop working?

Because the file it lived in doesn’t run anymore. Your old setup was theme code: checkout.liquid loaded the GTM container and pushed events such as begin_checkout or purchase whenever a checkout step rendered. The upgraded checkout is a Shopify-controlled app, so theme code, including checkout.liquid, no longer runs inside it.

The same upgrade retired Additional Scripts on the thank-you and order status pages, which is where many stores fired their purchase push. If conversions fell off a cliff on one specific date, why ad conversions drop to zero after Shopify’s thank-you page upgrade shows how to confirm that date against your orders.

What Shopify gives you instead is Customer Events: a stream of named events you read from a custom pixel, saved under Settings → Customer events in the admin. Code there runs in a sandbox, not on the checkout page. It can’t see the page’s DOM (Document Object Model), and it can’t read variables your theme sets. It only sees the events Shopify hands it.

Before you rebuild anything, export the old setup:

  1. List every push. Copy each dataLayer.push() from checkout.liquid and the order status scripts, with the keys each one sent.
  2. List every tag that fired on them. In GTM, filter tags by the triggers that listened to those events. That’s your real scope: often a GA4 tag, a Google Ads tag, a Meta tag and an affiliate tag or two.

Which Shopify events replace your old dataLayer pushes?

Shopify publishes a standard event for each checkout step, and each one carries the full checkout object. Map your old pushes to them one by one. The event names and descriptions below come from Shopify’s own web pixels type definitions (@shopify/web-pixels-extension 2.18.0):

Old dataLayer push Shopify standard event When it fires
begin_checkout checkout_started Customer enters checkout
contact step checkout_contact_info_submitted Customer submits a checkout form
address step checkout_address_info_submitted Customer submits a mailing address
add_shipping_info checkout_shipping_info_submitted Customer chooses a shipping rate
add_payment_info payment_info_submitted Customer submits payment information
purchase checkout_completed Visitor completes a purchase

Mapping of old checkout.liquid dataLayer pushes to Shopify's standard checkout events: begin_checkout to checkout_started, add_shipping_info to checkout_shipping_info_submitted, add_payment_info to payment_info_submitted and purchase to checkout_completed, plus the contact and address step events, shown as a dark dashboard-style custom pixel event map

Two details from the type definitions matter for a rebuild.

checkout_started can fire more than once. Shopify says that with Checkout Extensibility “this event is triggered every time a customer enters checkout”. A shopper who leaves and returns fires it again. If your old begin_checkout fired once per checkout, add your own guard, keyed on the checkout token.

The step events are extensibility-only. The contact, address and shipping events are “only available in checkouts where Checkout Extensibility for customizations is enabled”. On an upgraded checkout that’s you, but it’s why older tutorials don’t mention them.

Every event carries event.data.checkout, with lineItems, totalPrice, subtotalPrice, totalTax, currencyCode, email, phone, discountApplications and shippingLine. Each line item has a title, a quantity and a variant with its id, sku and price. That covers every key a GA4-style ecommerce push normally needs.

How do you rebuild the dataLayer inside a custom pixel?

Load GTM inside the pixel, then turn each Shopify event into the push your container already expects. That way your existing triggers and tags keep working with their event names unchanged. Here’s the pattern:

// Shopify admin → Settings → Customer events → Add custom pixel
const GTM_ID = 'GTM-XXXXXXX';
window.dataLayer = window.dataLayer || [];
(function(w,d,s,l,i){w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});
  const f=d.getElementsByTagName(s)[0], j=d.createElement(s);
  j.async=true; j.src='https://www.googletagmanager.com/gtm.js?id='+i;
  f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer',GTM_ID);

const items = (c) => c.lineItems.map((li) => ({
  item_id: li.variant?.sku || li.variant?.id,
  item_name: li.title,
  price: li.variant?.price?.amount,
  quantity: li.quantity
}));

const push = (name, c, extra = {}) => {
  window.dataLayer.push({ ecommerce: null });       // clear the last object
  window.dataLayer.push({
    event: name,
    page_location: c._href,                         // real URL, not the sandbox
    ecommerce: { currency: c.currencyCode, value: c.totalPrice?.amount,
                 items: items(c), ...extra }
  });
};

const MAP = {
  checkout_started: 'begin_checkout',
  checkout_shipping_info_submitted: 'add_shipping_info',
  payment_info_submitted: 'add_payment_info'
};
Object.keys(MAP).forEach((evt) => analytics.subscribe(evt, (e) => {
  push(MAP[evt], { ...e.data.checkout, _href: e.context.document.location.href });
}));

analytics.subscribe('checkout_completed', (e) => {
  const c = e.data.checkout;
  push('purchase', { ...c, _href: e.context.document.location.href },
       { transaction_id: c.order?.id, tax: c.totalTax?.amount,
         shipping: c.shippingLine?.price?.amount });
});

Test with a real order in GTM, then in each destination. Don’t trust a green checkmark in the pixel list alone; it tells you the code saved, not that tags fired.

What breaks when GTM runs inside the sandbox?

Anything in your container that assumes it’s running on the real page. The dataLayer pushes arrive fine. The trouble is tags and triggers that reach past the dataLayer to the page itself.

The usual breakages, in the order people hit them:

  • DOM triggers do nothing. Click, form submission and element visibility triggers watch the page. The sandbox has no view of the checkout’s buttons or fields, so those triggers never fire. Replace them with the Shopify event for that step.
  • Page URL variables read the sandbox. GTM’s built-in Page URL and Page Path describe the frame the container runs in, not the checkout. That’s why the pattern above passes event.context.document.location.href, which Shopify documents as a snapshot of the top frame’s document.
  • Custom HTML tags that read the page fail. A tag that scrapes the order total from the DOM, or reads a variable your theme set, finds nothing. Rewrite it to read from the dataLayer.
  • Debugging is harder. Preview mode and browser extensions are built around a container on a normal page. Expect to verify in each destination’s own debug view instead.

How do you keep custom pushes from your theme?

Publish them as Shopify custom events, then forward them from the pixel. If the storefront used to push things like newsletter_signup or size_guide_open, Shopify’s types describe custom events as those “emitted by partners or merchants via the publish method”. Your theme publishes, and the pixel subscribes:

// In the theme (storefront):
Shopify.analytics.publish('size_guide_open', { product: 'Trail Jacket' });

// In the custom pixel:
analytics.subscribe('all_custom_events', (e) => {
  window.dataLayer.push({ event: e.name, ...e.customData });
});

This keeps one container fed from one place. You don’t need a second GTM container in the theme sending the same events again.

Do you still need GTM in checkout at all?

Only if a tag in it does something you can’t do another way. Walk through the tag list you exported. For each one, ask what it’s for.

Analytics and affiliate tags that genuinely need checkout steps are the case for the custom-pixel rebuild above. Ad-platform conversion tags usually aren’t. A Meta, Google Ads or TikTok purchase tag in GTM is a browser event, and it still loses the sale when the tab closes before the order status page loads, a blocker strips it, or payment happens off-site. Moving it into a sandboxed container doesn’t change that.

For those, server-side sending is the sturdier replacement, and it doesn’t need a server-side GTM container either. Do you need Stape and GTM for Meta Conversions API walks through that trade-off.

One rule holds whichever you choose: one source per event. If your rebuilt container sends a Meta Purchase and a server-side feed sends the same Purchase with a different event ID, Meta counts both. Why Meta CAPI isn’t deduplicating covers how Meta pairs browser and server events, and why mismatched IDs double-count.

How does PartialLeads send Shopify checkout events without a dataLayer?

The PartialLeads Customer Events pixel subscribes to Shopify’s standard events itself and forwards the funnel steps server-side, so no GTM container or dataLayer has to run in checkout. checkout_started becomes InitiateCheckout and payment_info_submitted becomes AddPaymentInfo, sent from PartialLeads’ server to the ad platforms you’ve connected.

The pieces, step by step:

  1. Funnel events from the pixel. Alongside the checkout steps, the pixel path also sends ViewContent, AddToCart and Search. The ad platform gets the full funnel through the Conversions API, not just the sale.
  2. Purchases from the order webhook. The Purchase doesn’t depend on checkout_completed firing in a browser. Shopify sends the order to PartialLeads server-side, and every paid order (financial_status = paid) produces a conversion. Cash-on-delivery and bank-transfer orders are sent when they’re marked paid.
  3. Matched back to the ad click. The tag records UTMs and click IDs (fbclid, gclid, ttclid) at landing. The order is matched to that session by the visitor ID echoed from the storefront, then email, then phone. That’s how a sale gets credited to the campaign that started it. The wider picture is in tracking abandoned checkouts back to their source.

PartialLeads Meta CAPI activity log listing InitiateCheckout, AddPaymentInfo and Purchase events for a Shopify store with status dots, platform chips and timestamps, beside a health strip showing delivery rate and failed sends

The honest constraints. PartialLeads forwards the steps that map to ad-platform events. Keep a custom pixel if you also need the contact, address and shipping steps in GA4 or another analytics tool. The order value sent is the full order total, including tax and shipping, after discounts. There’s no subtotal option, and each order is recorded once, so a post-purchase upsell added to the same order doesn’t update it. PartialLeads never sends its own event twice, but it doesn’t de-duplicate against another app’s or pixel’s browser events. If your rebuilt container also sends Meta a Purchase, switch one off. And the split that matters: PartialLeads controls whether every paid order is sent. Whether the platform ties it to a person and a click depends on the identifiers captured and on the platform’s own matching.

Where you check it. The Meta CAPI page has an activity log with each event name, its status and the platform it went to, plus a health strip with delivery rate and failed sends. The Purchases ledger lists every paid order with its match tier. The Journey timeline on each lead shows the path from ad click to purchase. It’s one piece of the wider Shopify tracking, attribution and catalog fix.

What breaks The mechanism Where you see it in the dashboard
checkout.liquid dataLayer pushes stop running Customer Events pixel subscribes to checkout_started and payment_info_submitted directly Meta CAPI activity log: InitiateCheckout, AddPaymentInfo
Purchase tag misses closed tabs and off-site payments Purchase sent server-side from the order webhook for every paid order Purchases ledger: every paid order, with match tier
Sale credited to Direct, not the ad UTMs and click IDs captured at landing, order matched by visitor ID, email, phone Journey timeline per lead
Same Purchase counted twice PartialLeads’ own event IDs; keep one source per event Meta CAPI activity log: each Purchase sent once

Tell us what's broken. We'll fix your tracking — free.

Describe the tracking/attribution problem you're stuck on and we'll map it to a fix: server-side conversions to Meta, Google, TikTok and Pinterest, plus first-party tracking that survives Safari. No code required.

Sources


Frequently asked questions

QCan I still put a GTM container in Shopify's checkout?
Not in the checkout page itself. The upgraded checkout doesn't run checkout.liquid or theme code, so there's nowhere to paste the container snippet. You can load GTM inside a custom pixel under Customer events, where it runs in a sandbox and only sees the dataLayer pushes you build from Shopify's events. Click, form and page-based triggers won't work there.
QWhich Shopify event replaces begin_checkout?
checkout_started. Shopify fires it when a customer enters checkout, and on an upgraded checkout it fires every time they enter, not just the first time. If your old begin_checkout fired once per checkout, add a guard keyed on the checkout token so a shopper who leaves and returns doesn't count twice.
QWhy does GTM show a strange URL instead of my checkout page?
Because the container runs inside the custom pixel's sandbox, and GTM's built-in page variables describe that frame. Shopify includes a snapshot of the top frame's document with every event, so pass event.context.document.location.href into your dataLayer push and use that value in your tags instead of the built-in Page URL.
QHow do I send custom events from my theme to the checkout pixel?
Publish them from the theme with Shopify.analytics.publish, then subscribe to all_custom_events in the custom pixel and push each one into the dataLayer. That keeps one container fed from one place, instead of a theme container and a pixel container both sending the same events.
QWill rebuilding my GTM setup in a custom pixel fix missing purchases?
Not entirely. A custom pixel is still browser code, so it misses purchases when the tab closes before the order status page loads, when a blocker stops it, or when payment finishes off-site. It restores your old tracking, but it doesn't make it more complete. For ad-platform conversions, a server-side send from the order is sturdier.
QDoes PartialLeads replace GTM for GA4 checkout steps?
No. PartialLeads sends checkout funnel events to ad platforms server-side, and it has no GA4 integration. If you need the contact, address and shipping steps in GA4 or another analytics tool, keep a custom pixel for those, and let PartialLeads handle the ad-platform events so each event still has only one source.

Find the qualified leads your forms are currently throwing away.

Install PartialLeads on one landing page, send traffic, and compare what your CRM captured against what PartialLeads recovered and qualified.