Go to Settings > Customer events in the Shopify admin, click Add custom pixel, name it, and write JavaScript inside the sandboxed editor using the global analytics.subscribe(eventName, callback) API to listen for standard events such as page_viewed, checkout_started, and checkout_completed. The pixel runs in an isolated sandbox with no direct DOM access, so all data must come from the event payload, not from scraping the page.
Why this happens
Shopify built the Web Pixels API specifically to replace Additional Scripts and checkout.liquid injection, because arbitrary third-party JavaScript running directly in the storefront or checkout DOM was a performance and security liability - particularly on checkout, where merchants were effectively giving app developers PCI-adjacent access. Custom pixels sandbox all merchant and app code so it can only read a curated event.data payload and call a small set of approved browser APIs, never the raw document.
That trade-off is deliberate: Shopify accepts the loss of arbitrary DOM access in exchange for checkout being fast and locked down. Anyone rebuilding a tracking setup after this migration has to think in terms of "what event am I subscribing to and what's in its payload," not "what element am I reading off the page."
Fix it
- In the Shopify admin: Settings > Customer events > Add custom pixel. Give it a name - this is an internal label only, never shown to customers.
- In the code editor, register listeners for the events you need:
analytics.subscribe('page_viewed', (event) => {...}), and similarly forproduct_viewed,product_added_to_cart,checkout_started,checkout_completed, andpayment_info_submitted. - Inside each callback, read fields from
event.data. Forcheckout_completedthat's typicallyevent.data.checkout.order.id,.totalPrice.amount,.currencyCode, and.lineItems. - Send the event onward with
fetch()to your own endpoint or a vendor's collection URL. Fetch calls from inside the sandbox are restricted to domains you explicitly allow - Shopify will prompt you to add each new destination domain when you save. - If you need a cookie-based id or local storage for a client identifier, use the pixel's
browser.cookie.get/setandbrowser.localStorage.get/sethelpers -document.cookieandwindow.localStorageare not exposed inside the sandbox. - Save the pixel, then explicitly toggle it from "Disconnected" to "Connected." Pixels are created disconnected by default and will not fire until you turn them on.
- If a destination needs an image pixel (a plain 1x1 GET request) rather than a fetch call, the same allow-listed-domain rule applies - any request to an undeclared destination is blocked silently.
Branch: if you need one pixel to send data to several different vendors (analytics, ads, a warehouse), you don't need three separate custom pixels - one pixel can call fetch() multiple times to multiple allow-listed domains from inside the same event callback.
How to verify it worked
With the pixel connected, open the storefront in an incognito window with DevTools > Network open, filter by your destination domain, and walk through browse > add to cart > checkout > complete order. Confirm one network request fires per subscribed event, and that the checkout_completed request carries the correct order id and total matching the test order you just placed in Shopify admin. If a request never appears, check first whether the destination domain was actually added to the pixel's allow-list - a blocked fetch to an undeclared domain fails silently in the sandbox rather than throwing a visible console error in some cases.