Shopify Custom Pixels
Shopify’s Custom Pixels are sandboxed JavaScript environments that run inside a restricted iframe during checkout and on storefront pages. They’re Shopify’s answer to the tracking limitations created by Checkout Extensibility — a controlled way to run third-party JavaScript in the checkout while maintaining security and performance guarantees.
Understanding what Custom Pixels can and cannot do is the difference between a working checkout tracking implementation and hours of debugging.
What Custom Pixels are
Section titled “What Custom Pixels are”Custom Pixels run in a web worker-like sandbox. You create them in Shopify Admin under Marketing > Customer Events. Each pixel is a JavaScript file that Shopify loads in a restricted iframe.
The sandbox has strict limitations:
- No DOM access — no
document.querySelector, nodocument.getElementById - No
window.location— you cannot read the current URL directly - No direct cookie access — cookies are restricted
- No
localStorageorsessionStorage - Fetch is available — you can make network requests
analyticsAPI is available — Shopify’s Customer Events subscription API
GTM inside a Custom Pixel
Section titled “GTM inside a Custom Pixel”You can load GTM inside a Custom Pixel, but the restrictions affect what GTM can do.
// Custom Pixel script// GTM loads in the sandbox(function(w,d,s,l,i){ w[l]=w[l]||[]; w[l].push({'gtm.start': new Date().getTime(), event:'gtm.js'}); var f=d.getElementsByTagName(s)[0], j=d.createElement(s), dl=l!='dataLayer'?'&l='+l:''; j.async=true; j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl; f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','GTM-XXXXXXX');GTM triggers that work in Custom Pixels:
- Custom Event triggers (the only reliable option)
- Page View trigger (fires on pixel initialization)
GTM triggers that do NOT work:
- Click triggers (no DOM access — no click delegation)
- Form triggers (no DOM access)
- Scroll triggers (no scroll events)
- DOM Ready / Window Loaded (restricted environment)
The practical implication: if you load GTM in a Custom Pixel, build your entire checkout tracking on Custom Event triggers fired by the analytics.subscribe() API.
The Customer Events API
Section titled “The Customer Events API”The analytics.subscribe() API is the correct way to receive Shopify checkout events inside a Custom Pixel.
// Subscribe to Shopify's ecommerce eventsanalytics.subscribe('checkout_completed', (event) => { const checkout = event.data.checkout;
const items = checkout.lineItems.map((item, index) => ({ item_id: item.variant?.sku || item.variant?.id || item.id, item_name: item.title, item_variant: item.variant?.title !== 'Default Title' ? item.variant?.title : '', price: parseFloat(item.variant?.price?.amount || 0), quantity: item.quantity, index: index, }));
window.dataLayer = window.dataLayer || []; dataLayer.push({ ecommerce: null }); dataLayer.push({ event: 'purchase', ecommerce: { transaction_id: checkout.order?.id || checkout.token, value: parseFloat(checkout.totalPrice?.amount || 0), tax: parseFloat(checkout.totalTax?.amount || 0), shipping: parseFloat(checkout.shippingLine?.price?.amount || 0), currency: checkout.currencyCode, coupon: checkout.discountApplications?.[0]?.title || '', items: items, }, });});Available Customer Events
Section titled “Available Customer Events”Shopify provides these standard events via the analytics.subscribe() API:
| Event | When it fires |
|---|---|
page_viewed | Any page view in the checkout flow |
cart_viewed | Cart page view |
checkout_started | Checkout initiation |
checkout_address_info_submitted | Shipping address completed |
checkout_shipping_info_submitted | Shipping method selected |
checkout_contact_info_submitted | Contact info completed |
payment_info_submitted | Payment info entered |
checkout_completed | Order confirmed |
product_viewed | Product detail page view (storefront pixel only) |
product_added_to_cart | Add to cart (storefront pixel only) |
product_removed_from_cart | Remove from cart (storefront pixel only) |
Mapping Shopify events to GA4 events
Section titled “Mapping Shopify events to GA4 events”// checkout_started → begin_checkoutanalytics.subscribe('checkout_started', (event) => { const checkout = event.data.checkout;
dataLayer.push({ ecommerce: null }); dataLayer.push({ event: 'begin_checkout', ecommerce: { currency: checkout.currencyCode, value: parseFloat(checkout.subtotalPrice?.amount || 0), items: checkout.lineItems.map((item, i) => ({ item_id: item.variant?.sku || String(item.variant?.id), item_name: item.title, price: parseFloat(item.variant?.price?.amount || 0), quantity: item.quantity, index: i, })), }, });});
// checkout_shipping_info_submitted → add_shipping_infoanalytics.subscribe('checkout_shipping_info_submitted', (event) => { const checkout = event.data.checkout; const shippingLine = checkout.shippingLine;
dataLayer.push({ ecommerce: null }); dataLayer.push({ event: 'add_shipping_info', ecommerce: { currency: checkout.currencyCode, value: parseFloat(checkout.subtotalPrice?.amount || 0), shipping_tier: shippingLine?.title || '', items: checkout.lineItems.map((item, i) => ({ item_id: item.variant?.sku || String(item.variant?.id), item_name: item.title, price: parseFloat(item.variant?.price?.amount || 0), quantity: item.quantity, index: i, })), }, });});Custom Pixel vs. theme-based GTM vs. server-side
Section titled “Custom Pixel vs. theme-based GTM vs. server-side”| Approach | Storefront | Checkout | Reliability | Setup complexity |
|---|---|---|---|---|
| Theme-based GTM | ✅ Full | ❌ Non-Plus only | High | Low |
| Custom Pixel (GTM inside) | ✅ Limited | ✅ Standard & Plus | Medium | Medium |
| Custom Pixel (no GTM) | ✅ Good | ✅ Standard & Plus | High | Low-Medium |
| Server-side (sGTM) | ✅ Yes | ✅ Yes | Very High | High |
For most standard Shopify merchants: use theme-based GTM for the storefront and Custom Pixels for checkout events.
Debugging Custom Pixels
Section titled “Debugging Custom Pixels”GTM Preview Mode does not connect to GTM containers loaded inside Custom Pixel sandboxes. Use browser DevTools instead.
-
Open Chrome DevTools (F12).
-
In the Console tab, click the JavaScript context dropdown (it says “top” by default).
-
Switch to the Custom Pixel iframe context — it appears as a Shopify subdomain or
sandbox-*label. -
Now
console.log(dataLayer)shows the pixel’s local dataLayer, and you can inspect events as they fire.
Add a temporary debug pixel to log all events:
// Temporary debug pixel — remove before going liveanalytics.subscribe('all_events', (event) => { console.log('[Pixel Debug]', event.name, JSON.stringify(event.data, null, 2));});See Shopify Custom Pixels: Debugging & Sandbox Guide for the complete debugging reference including postMessage workarounds and common error fixes.
Common mistakes
Section titled “Common mistakes”Trying to use DOM-based GTM triggers in Custom Pixels. Click triggers, form triggers, and scroll triggers all fail silently. You won’t see errors — the triggers just never fire because GTM’s click delegation can’t attach to the DOM.
Not handling the Default Title variant. Shopify uses “Default Title” as the variant title for products with no variants. Always filter this out when constructing your items array.
Duplicating checkout_completed and order-status.liquid. If you have a Custom Pixel that tracks checkout_completed AND an order-status.liquid script that also tracks purchase, you’ll get duplicate purchase events. Implement one or the other, not both.