Shopify Web Pixels: App Pixel or Custom Pixel?

A Shopify web pixel extension is tracking code that runs inside a sandbox Shopify controls, not in the storefront page. App pixels ship inside your app, run in a strict web worker, and read their configuration from the Admin API. Custom pixels are pasted into the admin by a merchant and run in a lax iframe. Neither one can read or write the DOM.

That last sentence is what breaks ported code. Shopify’s about web pixels page says it plainly: anything relying on “scraping the DOM for information or attempting to write to the DOM” either behaves differently or stops working.

There is a deadline attached. scriptTagCreate and scriptTagUpdate start returning a user error on 1 October 2026, and Shopify stops injecting script tags into storefronts on 1 March 2027, according to the deprecation changelog. For anything that only collects analytics or conversion data, Shopify’s stated replacement is a web pixel, which “needs no action from the app user”. Everything else belongs in an app embed block, which we covered in our write-up on theme app extensions.

What a Shopify web pixel extension can and cannot do

App pixels load in the strict sandbox, a web worker. Shopify guarantees only these globals are present: self, console, the timer functions (setTimeout, clearTimeout, setInterval, clearInterval), and fetch along with Headers, Request and Response. The docs are explicit that you must not rely on any other global being available, since many are explicitly overwritten to undefined, and other non-language globals might be overwritten at any time. Requests out of the worker need CORS support on the receiving end, exactly as they would from any other fetch.

So window.document is gone. So is reading a data layer off the page, waiting for a DOM node to appear, or writing a tracking iframe into the body.

What replaces it is data Shopify hands you. The init object is a snapshot of the page at render time, with init.context carrying read-only document, navigator and window details, and init.data carrying cart, customer, shop and purchasingCompany. Cookies and web storage are still reachable through browser.cookie, browser.localStorage and browser.sessionStorage. Those calls execute asynchronously in the top frame, so you await them.

Custom pixels get the lax sandbox instead: an iframe carrying the sandbox attribute with allow-scripts and allow-forms. Legacy pixels survive there, but they cannot reach the top frame, so certain properties return different values. Shopify’s example is window.href, which returns the sandbox URL rather than the top frame URL. Anything reading the current page out of the browser needs checking against that.

App pixel or custom pixel?

Build an app pixel when you own an app. It installs with the app and updates when you deploy. It is also the only one of the two with access to the settings property, which is how a merchant’s account ID or API key reaches the sandbox without anyone editing code.

A custom pixel is the merchant’s own. They create it in the admin’s pixel manager and paste JavaScript into it, with the privacy options set through the same interface. That suits a one-off vendor tag on a single store. It does not suit anything you have to maintain across a client list, because every change is a manual edit in someone else’s admin.

Shopify’s own recommendation, in the same document that describes the two sandboxes, is to “create apps and encourage users to install their apps instead of creating Custom Pixels, for greater integration”.

Which events can you subscribe to?

Fifteen standard events, as of September 2026: page_viewed, product_viewed, collection_viewed, search_submitted, cart_viewed, product_added_to_cart, product_removed_from_cart, checkout_started, checkout_address_info_submitted, checkout_contact_info_submitted, checkout_shipping_info_submitted, payment_info_submitted, checkout_completed, alert_displayed and ui_extension_errored.

The subscribe signature is (eventName: string, event_callback: Function) => Promise<undefined>. Four aggregate names also work: all_events, all_standard_events, all_custom_events and all_dom_events. Subscribing to all_events is convenient and slightly risky, because the docs warn that the contents of those aggregate subscriptions change as events are added or modified.

You can also publish your own. A theme calls Shopify.analytics.publish('my_store:event_name', event_data), and a pixel subscribes to my_app:my_custom_event, receiving whatever was published on the event’s customData field. Prefixing keeps custom names from colliding with standard ones.

import {register} from '@shopify/web-pixels-extension';

register(({analytics, browser, settings, init}) => {
  let consent = init.customerPrivacy;

  analytics.subscribe('checkout_completed', async (event) => {
    if (!consent.analyticsProcessingAllowed) return;

    const visitorId = await browser.cookie.get('my_visitor_id');

    await fetch('https://example.com/collect', {
      method: 'POST',
      body: JSON.stringify({
        accountId: settings.accountID,
        event: event.name,
        checkoutToken: event.data.checkout.token,
        visitorId,
      }),
      keepalive: true,
    });
  });
});

Why your pixel might never load

Consent is enforced before your code runs, not inside it. The pixel privacy docs state that Shopify’s pixel manager “will only load your pixel if there is visitor permission for all of the settings that your pixels declares as required”.

Each true in the [customer_privacy] block is another condition the visitor has to satisfy before the pixel loads at all. Declaring every permission because it looks safer costs you data.

When consent changes mid-session, which is what a cookie banner does, nothing re-runs on its own. Read the starting state from init.customerPrivacy, then subscribe for updates. Only one event name is accepted today: visitorConsentCollected. Its payload carries analyticsProcessingAllowed, marketingAllowed, preferencesProcessingAllowed and saleOfDataAllowed.

Shipping it

Generate the extension, then declare what it needs:

shopify app generate extension --template web_pixel --name my-web-pixel
type = "web_pixel_extension"
name = "my-web-pixel"
runtime_context = "strict"

[customer_privacy]
analytics = true
marketing = false
preferences = false
sale_of_data = "enabled"

[settings]
type = "object"

[settings.fields.accountID]
name = "Account ID"
description = "Account ID"
type = "single_line_text_field"
validations = [
  { name = "min", value = "1" }
]

single_line_text_field is the only settings type web pixels support, so anything structured goes in as a JSON string and gets parsed in the worker.

The extension does nothing until a record exists on the store. Create it once per shop after install, against the 2026-07 Admin API, which is the latest stable version and is accessible until 16 July 2027:

mutation {
  webPixelCreate(webPixel: { settings: "{\"accountID\":\"123\"}" }) {
    userErrors {
      code
      field
      message
    }
    webPixel {
      id
      settings
    }
  }
}

Shopify validates that settings string against the schema in shopify.extension.toml, and the mutation fails if the two disagree. Use webPixelUpdate with the pixel’s ID when a merchant changes their account details, and note that reading the WebPixel object back needs the read_pixels scope.

Build an app pixel if you ship an app and want analytics that survive both the script tag sunset and a theme change. Use a custom pixel only for a single merchant’s one-off vendor tag. Do not port an existing tag line for line, because a DOM-reading pixel cannot work in a worker; rewrite it around the event payloads and init. And if what you actually need is to render something a customer sees, a pixel is the wrong extension point entirely, which is where checkout UI extensions come in.

We build and migrate Shopify apps, including the unglamorous work of moving analytics off script tags before the March 2027 cutoff without losing conversion data in the process. If you want a second pair of eyes on a pixel migration, that is part of our Shopify development work.

Need this built properly?

Whoooop Ltd has spent 15+ years building and maintaining web applications in TypeScript, React, Node.js and serverless — the same ground this post covers.

Get in touch