Shopify Custom Pixel — how Admaxxer’s pixel works in Shopify’s sandbox
Three rabbit holes you don’t want to fall into yourself. Shopify’s Custom Pixel runtime is a sandboxed Web Worker — no document, no DOM, no <script>-tag loading. Only analytics.subscribe() and fetch() are available. We learned this the hard way across three production incidents (GL#305, GL#306, GL#307). This page documents the runtime, the three install errors merchants commonly hit, how the canonical snippet works, and where to copy your auto-filled version.
TL;DR
- Two install paths exist. Vintage themes (still on theme.liquid) get a regular
<script defer>tag pasted into the layout. Modern Online Store 2.0 themes use Shopify’s Customer Events Custom Pixel — pasting the script-tag form into the Custom Pixel field is one of our most-common install errors. - Custom Pixel runs in a Web Worker, not in the page. The worker has no
documentobject, sodocument.createElement('script')throws silently and the merchant loses 100% of their pixel data. The canonical snippet uses Shopify’sanalytics.subscribe()API andfetch()directly. - The Worker scope strips Origin and Referer headers from outbound
fetch()calls. Our server-sideoriginAllowed()CORS check now falls back to a body-sidehostfield that the snippet always includes. - The destructure pattern is load-bearing. Bracket notation (
event["data"]) defeats chat-client autolinkers but trips Shopify’sdot-notationESLint rule. Dot notation (event.data) satisfies the rule but autolinkers wrap it in a hyperlink. Destructuring (const { data } = event) satisfies both. - Save BEFORE Connect. Shopify rejects the Connect button if the snippet hasn’t been saved first. Our 5-step install card on
/integrations/shopifywalks you through the order.
Section 1 — The Shopify Custom Pixel runtime (educational)
Before debugging anything, understand which install path you’re on. Shopify ships two completely separate pixel install paths and they have different runtime semantics.
Path A — legacy theme.liquid script-tag (vintage themes)
If your store is still on a vintage theme (pre-Online Store 2.0), you can paste a regular <script defer src="..."></script> tag into layout/theme.liquid just before </head>. This is the same model as WordPress, Webflow, or any vanilla HTML page — the script runs in the storefront window, has full document access, and Admaxxer’s universal pixel script does the rest.
This path is dwindling. Most stores have already migrated to Online Store 2.0, where theme.liquid edits are discouraged and the pixel ought to live in Customer events.
Path B — modern Customer Events Custom Pixel (Online Store 2.0)
Shopify Admin › Settings › Customer events › Add custom pixel. This is where every modern store should install Admaxxer. The runtime model is fundamentally different from Path A:
- Your code runs in a sandboxed Web Worker, NOT in the storefront window. Workers have no DOM, no
document, nowindowas you know it from a regular page script. - The only globals available are
analytics(Shopify’s pub-sub for documented customer events —page_viewed,checkout_completed,product_viewed, etc.) andfetch(). - You cannot inject another script via
document.createElement('script')— there is nodocumentfor the call to attach to. The call throws silently inside the worker and Shopify surfaces the warning “Pixel will not track any customer behavior because it is not subscribed to any events.” - The fetch sandbox strips identifying headers (Origin, Referer) under Shopify’s default referrer policy. Your endpoint will see a header-less request and must auth on something other than Origin.
Why Shopify did this: three reasons combine.
- Privacy. Workers can’t read cookies set by the merchant’s ad platforms or steal data the storefront has access to via the DOM.
- Performance. A misbehaving pixel that spins a render-loop or holds the main thread can’t affect storefront paint times because it runs on a background thread.
- Sandboxing third-party code. Custom Pixel scripts are user-supplied and merchant-pasted — running them in a worker means a buggy or malicious snippet can’t take down the storefront.
The trade-off is that pixel snippets that look correct in a vintage-theme world (script-tag injection, document-side cookies, Origin-checked APIs) all stop working. Admaxxer’s canonical snippet is built for the worker model from the ground up.
Section 2 — Three install errors merchants commonly hit (the saga)
Three production incidents shaped the current snippet. Each had a different root cause and a different fix. If you’re debugging a non-firing Admaxxer pixel on Shopify, you’re almost certainly hitting one of these.
(a) “Pixel will not track any customer behavior because it is not subscribed to any events” — GL#305
Symptom: this exact yellow warning at the top of Shopify Admin › Settings › Customer events › (your custom pixel). Status reads “Disconnected” even after you paste a snippet that looks like working JavaScript.
Cause: the pasted snippet wraps a document.createElement('script') + document.head.appendChild(s) shape, or worse, raw <script defer src="..."></script> HTML. Both fail in the Web Worker sandbox — there is no document, no head, no DOM at all. The worker silently rejects the call (no error surfaced to Shopify Admin), no analytics.subscribe() listener is ever registered, and Shopify’s “not subscribed to any events” check fires. The merchant loses 100% of pixel events, often without realising for days.
Fix: paste the canonical analytics.subscribe() snippet from /integrations/shopify (your real admx_… website ID auto-filled) instead. The snippet calls analytics.subscribe() directly inside the worker for page_viewed, checkout_completed, and product_viewed, then fetch()’s the event payload to https://admaxxer.com/api/event. No DOM access required.
Permanent guardrail: a build-time canary at scripts/verify-no-shopify-script-tag-snippet.ts fails the build if document.createElement('script') shows up in any user-facing client or SSR template snippet. Every consumer (server SSR templates, client UI components, the GET /api/integrations/shopify/custom-pixel-snippet endpoint) imports from shared/shopify-custom-pixel-snippet.ts — the single source of truth.
(b) “Did not load. An error occurred. Events received: 0” — GL#306
Symptom: Shopify’s Pixel Inspector (the panel beneath your Custom Pixel) shows “Did not load” with an error indicator. Even after pasting the canonical snippet, “Events received” stays at 0.
Cause 1 (parse error): the merchant copied the snippet from a chat-client surface (Slack, Discord, our own AI chat drawer) where the autolinker had mangled property accesses ending in real ICANN gTLDs — event.data became [event.data](http://event.data), checkout.email became checkout.[email](http://email). Shopify’s editor parses this as JS, finds a syntax error, and refuses to load the pixel.
Cause 2 (CORS rejection): Shopify’s Worker fetches strip Origin and Referer headers under the default referrer policy. Our server-side originAllowed() check (in server/routes/pixelIngest.ts) used to look at Origin and Referer first, then 403 the request when both were absent. So even a parse-clean snippet was getting rejected at the network layer.
Fix:
- Copy from the dashboard, not from chat. The dashboard surface is a regular HTML
<pre>block with no autolinker pass — what you see is byte-identical to what Shopify will see. - Body-side host fallback. The canonical snippet now adds a
host: location.hostnamefield to every event body. Server-sideresolveAndCheckOrigin(inserver/routes/pixelIngest.ts) tries Origin first, then Referer, thenbody.hostas the third fallback.originAllowed()matches againstpixel_websites.allowed_domains.
Validation: on 2026-04-29 at 03:52:21 UTC, Donnyfeathers’ first real visitor pageview landed in Tinybird. The Shopify Custom Pixel sandbox fired page_viewed, our snippet POST’d to /api/event with host: "donnyfeathers.com" in the body, the body-side host fallback kicked in, originAllowed() matched against allowed_domains, and Tinybird wrote the row with accepted: true.
(c) ["data"] is better written in dot notation ESLint warnings — GL#307
Symptom: Shopify’s Custom Pixel editor shows multiple yellow ESLint warnings on every line that uses bracket notation:
["data"] is better written in dot notation["email"] is better written in dot notation["product"] is better written in dot notation
Pixel still loads, events still flow — but the editor is loud and the merchant gets nervous. Worse: even though warnings are non-blocking today, Shopify could decide to upgrade them to errors in a future release and silently break the pixel.
Cause: bracket notation was introduced as a defence against chat-client autolinkers. Slack, Discord, GitHub, our own AI chat drawer — they all auto-link any text matching xxx.tld where tld is a real ICANN gTLD. .data, .id, .email, .product are all real gTLDs. So event.data in chat became [event.data](http://event.data), breaking the snippet (cause 2(b) above). Switching to event["data"] defeated the autolinker. But Shopify’s Custom Pixel editor enforces ESLint’s dot-notation rule, which rejects bracket notation for literal property names.
Two checks must pass simultaneously:
- Shopify ESLint
dot-notationrule — rejectsevent["data"], demandsevent.data. - Chat-client autolinker — auto-links any expression ending in
.data/.id/.email/.productbecause all four are real ICANN gTLDs.
Neither bracket nor dot satisfies both. The fix is the third option: destructuring.
Fix: destructure the offending properties into bare-name locals at the top of each event handler:
analytics.subscribe("checkout_completed", function (event) {
const { data } = event; // bare-name local
if (!data) return;
const { checkout } = data;
if (!checkout) return;
const { order, email: customerEmail, totalPrice, currencyCode } = checkout;
const { id: orderId } = order || {};
// … use customerEmail, orderId everywhere afterwards
});
The destructure satisfies the ESLint rule (no brackets, dot-equivalent left-hand-side). It also satisfies the autolinker (no expression in the body ends in a real gTLD — customerEmail ends in ‘Email’, not .email; orderId ends in ‘Id’, not .id). Both checks pass. Don’t “simplify” this back to inline access.
Section 3 — Common-errors troubleshooting matrix
Quick lookup table for the symptoms you might see in Shopify Admin or the Pixel Inspector. The Cause and Fix columns link back to the saga sections above.
| Symptom | Cause | Fix |
|---|---|---|
| “Pixel will not track any customer behavior because it is not subscribed to any events.” | Pasted document.createElement('script') or raw <script> HTML into the Custom Pixel field. The Web Worker has no DOM. (GL#305) |
Re-paste the canonical analytics.subscribe() snippet from /integrations/shopify with your real admx_… website ID. Click Save first, then Connect. |
| “Did not load. An error occurred.” in the Pixel Inspector. Events received: 0. | JS parse error from chat-client autolinker mangling the snippet (e.g. event.data → [event.data](http://event.data)). (GL#306, parse cause) |
Copy the snippet from the Admaxxer dashboard, not from chat. The dashboard <pre> block is byte-identical to what Shopify will receive. |
403 from /api/event in the Pixel Inspector network log. |
Shopify Worker fetches strip Origin and Referer headers, so server-side originAllowed() couldn’t identify the source domain. (GL#306, CORS cause) |
The canonical snippet now includes host: location.hostname in the body. Server-side resolveAndCheckOrigin uses this as a third fallback after Origin and Referer. |
["data"] is better written in dot notation ESLint warnings on multiple lines. |
Bracket notation defeats chat autolinkers but trips Shopify’s dot-notation rule. (GL#307) |
Re-paste the current canonical snippet (post-2026-04-29). It uses destructuring to satisfy both checks: const { data } = event instead of event.data or event["data"]. |
| “Pixel cannot be connected until a custom pixel script is saved for it.” | You clicked Connect before clicking Save. Shopify requires the snippet be saved first. | Click Save (top right, separate button). The Connect button activates only after a save. See Section 7. |
| Pixel saved + connected, but Tinybird shows zero pageviews. | Most likely your pixel_websites.allowed_domains doesn’t include the storefront hostname. (Less common: ad-blocker on your test browser.) |
Open /integrations/shopify in the dashboard. The 5-step card’s Step 5 polls pixel_websites.first_event_at live and shows the last allowed domain check. Add the missing domain in Settings › Pixel. |
Section 4 — Where to find the auto-filled snippet
The canonical snippet body lives in shared/shopify-custom-pixel-snippet.ts and is rendered by three surfaces. Always copy from one of these — never from chat, email, or a third-party guide.
- /integrations/shopify — the in-app integrations tab. After connecting Shopify via Custom App OAuth, the page renders a 5-step install card with the snippet pre-filled with your real workspace website ID (e.g.
admx_a1b2c3d4…). Step 5 pollspixel_websites.first_event_atlive and flips green when the merchant’s first real visitor lands. - /documentation/install/shopify — the public install guide. For anonymous visitors and crawlers; the snippet shows a placeholder
admx_YOUR_WEBSITE_IDstring. Has a Path A vs Path B callout (vintage script-tag vs modern Custom Pixel) and the same common-errors troubleshooting matrix as this page. GET /api/integrations/shopify/custom-pixel-snippet— programmatic JSON endpoint, session-auth required. Returns the snippet body filled with the requesting workspace’s real website ID. Useful for partner integrations and CI checks. Body shape:{ snippet: string, websiteId: string }.
If you’re reading this page logged-out, the snippet block below is the placeholder version. Sign in to /integrations/shopify to get the auto-filled version with your real admx_… ID.
// Admaxxer Custom Pixel — paste into Shopify Admin
// → Settings → Customer events → Add custom pixel
const WEBSITE_ID = "admx_YOUR_WEBSITE_ID";
const ENDPOINT = "https://admaxxer.com/api/event";
function send(payload) {
try {
fetch(ENDPOINT, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(payload),
}).then(function () {}, function () {});
} catch (e) {}
}
analytics.subscribe("page_viewed", function (event) {
const loc = event.context.window.location;
send({
website_id: WEBSITE_ID,
host: loc.hostname,
event_type: "pageview",
path: loc.pathname,
referrer: event.context.document.referrer,
});
});
analytics.subscribe("checkout_completed", function (event) {
const { data } = event; // GL#307 destructure pattern
if (!data) return;
const { checkout } = data;
if (!checkout) return;
const { order, email: customerEmail, totalPrice, currencyCode } = checkout;
const { id: orderId } = order || {};
const loc = event.context.window.location;
send({
website_id: WEBSITE_ID,
host: loc.hostname, // GL#306 host fallback
event_type: "payment",
amount_cents: Math.round(parseFloat(totalPrice.amount) * 100),
currency: currencyCode,
provider: "shopify",
external_payment_id: orderId,
email: customerEmail || undefined,
});
});
Excerpt only — the full snippet adds analytics.subscribe("product_viewed", …) for PDP tracking. The complete version is on /integrations/shopify.
Section 5 — How the canonical snippet works (architecture)
The snippet subscribes to three Shopify-documented Customer Events and forwards each to Admaxxer’s /api/event endpoint. Each subscription is independent — you can comment one out without breaking the others.
analytics.subscribe("page_viewed", …)
Fires on every storefront page-load. Sends event_type: "pageview", the path, the referrer, and the host. Powers the Pageviews tile, Sources tile, and the funnel-coverage check on /marketing-acquisition.
analytics.subscribe("checkout_completed", …)
Fires when an order completes. Sends event_type: "payment", the order’s amount in cents, currency, the Shopify order ID as external_payment_id, and the customer email. Gives you Shopify orders directly from the worker without needing to also configure a webhook subscription on the Custom App side — useful for cross-validating the two paths and surfacing attribution leakage.
analytics.subscribe("product_viewed", …)
Fires on PDP loads. Sends event_type: "custom", goal_name: "product_viewed", and the product ID. Powers the Top Products tile and the LTV-by-product breakdown.
Why fetch() instead of navigator.sendBeacon()?
sendBeacon is ideal for unload-time pings but isn’t available in the Worker scope. Web Workers expose fetch() only. We swallow errors with the .then(noop, noop) pattern so a single failed event never breaks the listener — subsequent events keep flowing.
Body shape (server validates)
The body shape matches pixelEventSchema in server/lib/pixel/validate.ts. Key fields:
website_id— required.admx_…. Identifies the workspace.host— required (GL#306). The storefront hostname. Used as the third Origin fallback for the CORS check.event_type— one ofpageview,payment, orcustom.path,referrer— for pageviews.amount_cents,currency,provider,external_payment_id,email— for payments.goal_name,product_id— for custom events.
If you change a field, update shared/shopify-custom-pixel-snippet.ts AND server/lib/pixel/validate.ts in the same PR. They’re a contract, not a coincidence.
Section 6 — Why the destructure pattern (GL#307)
Recap from Section 2(c): two checks must pass simultaneously for the snippet to render warning-free in Shopify’s editor AND parse cleanly when copied through any chat surface.
Check 1 — Shopify ESLint dot-notation rule
Shopify’s Custom Pixel editor enforces ESLint’s dot-notation rule. Any literal property access via brackets is flagged: event["data"] → warning, demand event.data.
Check 2 — chat-client autolinker
Slack, Discord, GitHub Markdown, our own AI chat drawer — every chat surface auto-links any expression of the form word.tld where tld is a real ICANN gTLD. As of April 2026, .data, .id, .email, .product are all real gTLDs (sold by Donuts, Verisign, and others). So a snippet like event.data.checkout typed in Slack becomes [event.data](http://event.data).checkout — copy-pasted into Shopify, that’s a syntax error.
Why neither bracket nor dot alone works
- Dot notation (
event.data) — ESLint passes. Autolinker fails (links the.datasuffix). - Bracket notation (
event["data"]) — Autolinker passes (no.tldsuffix). ESLint fails (dot-notation rule). - Destructure (
const { data } = event) — ESLint passes (LHS is a destructure pattern, not a member expression; the rule doesn’t fire). Autolinker passes (no.tldsuffix anywhere;customerEmailends in ‘Email’, not.email).
Renaming inside the destructure
For properties whose name is itself a gTLD suffix when concatenated (id, email, product), we rename inside the destructure to a bare-name local that doesn’t end in the suffix:
const { id: orderId } = order || {}; // bare-name: orderId
const { email: customerEmail } = checkout; // bare-name: customerEmail
const { id: productId } = product || {}; // bare-name: productId
From the autolinker’s perspective there’s no .id or .email substring anywhere — just orderId and customerEmail. From ESLint’s perspective the renaming is a destructure pattern, not bracket notation, so the rule doesn’t fire.
The transferable lesson
When two linters or parsers disagree on the same syntax, neither bracket nor dot satisfies both — destructure is the only common-denominator. We logged this as GL#307 in docs/KEY-LESSONS-PLATFORM.md as a meta-pattern for any future case where chat-client autolinkers and downstream linters fight over the same expression.
Section 7 — Save before Connect (the order matters)
Shopify rejects the Connect button until the snippet has been saved first. Click them in the wrong order and you get an error: “Pixel cannot be connected until a custom pixel script is saved for it.” Annoying, but harmless — click Save, then Connect.
The 5-step install flow
- Open Customer events in Shopify Admin. Settings › Customer events › Add custom pixel. Name it “Admaxxer”.
- Paste the snippet. Copy the JavaScript code from
/integrations/shopify(auto-filled with your real website ID) and paste into the code editor. Replace anything already there. - Click Save (top right). First button to click. Without saving first, the next step errors.
- Click Connect (separate button). Status flips from “Disconnected” to “Connected” and events start flowing.
- Verify. Open your storefront in an incognito tab. The Admaxxer dashboard’s Sources tile should show a new pageview within 5–10 seconds. Step 5 of
/integrations/shopifypollspixel_websites.first_event_atand flips green automatically when the first real visitor event lands.
The 5-step card on /integrations/shopify is animated framer-motion with each step revealing as you scroll. Use it — it’s the same content as this page but interactive.
Section 8 — FAQ
Q: Why did my Shopify Custom Pixel say “not subscribed to any events” even though my snippet looks fine?
You almost certainly pasted a script-tag-style snippet (document.createElement('script') or raw <script> HTML). Shopify’s Custom Pixel runs in a sandboxed Web Worker with no DOM — the call throws silently. Re-paste the canonical analytics.subscribe() snippet from /integrations/shopify. See GL#305.
Q: Why does the snippet destructure event.data instead of just using event.data directly?
Two checks must pass simultaneously: Shopify’s ESLint dot-notation rule (rejects bracket notation) AND chat-client autolinkers (mangle expressions ending in real ICANN gTLDs like .data, .id, .email, .product). Destructuring is the only syntax that satisfies both. See GL#307.
Q: My pixel is connected but I’m getting 403 errors on /api/event. Why?
Shopify’s Worker scope strips Origin and Referer headers from outbound fetch calls. The canonical snippet (post-2026-04-29) includes a host: location.hostname field in every body so the server has a third fallback for the CORS check after Origin and Referer. Re-paste the current snippet from /integrations/shopify. See GL#306.
Q: Should I use Path A (theme.liquid script-tag) or Path B (Customer Events Custom Pixel)?
Path B (Customer Events Custom Pixel) for any modern Online Store 2.0 theme. Shopify is migrating away from theme.liquid pixel installs, and the Customer Events path gives you cleaner separation between storefront rendering and pixel logic. Path A is still supported for vintage themes but isn’t recommended for new installs.
Q: Does the snippet send any data to Shopify itself?
No. The snippet runs inside Shopify’s Web Worker and uses Shopify’s analytics.subscribe() pub-sub to read the customer event payload, but every fetch() goes directly to https://admaxxer.com/api/event. Shopify is the host environment, not a recipient of the data.
Q: Will Shopify’s ESLint warnings break my pixel in the future?
Today the warnings are non-blocking. The current canonical snippet generates zero warnings (the destructure pattern bypasses the dot-notation rule entirely), so even if Shopify upgrades warnings to errors in a future release, your pixel will keep loading without changes.
Q: Where do I get the snippet auto-filled with my real website ID?
Open /integrations/shopify in the Admaxxer dashboard after connecting Shopify via the Custom App OAuth flow. The 5-step install card has a <pre> block with your admx_… website ID baked in — copy from there, not from chat or third-party docs.
Q: Does this page apply to the Shopify App Store install too?
The Shopify App Store distribution uses the same Customer Events Custom Pixel under the hood — the App just OAuths in and pastes the same canonical snippet on your behalf. So everything on this page applies. The App Store path is documented separately at /documentation/shopify-custom-app.
Related documentation
Install guide for Shopify · Shopify Custom App walkthrough · Tinybird auth model · Bring your own Anthropic key (BYOK) · Documentation home