Skip to main content
Your headless storefront runs on your infrastructure, so Shopify’s Web Pixel never loads there. It still covers checkout and order events — those pages are Shopify-hosted — but everything before checkout is invisible unless you report it. There are two ways to do that. Both send to the same place; they differ in where the events come from.
You do not need to instrument checkout. Shopify hosts it, so our app’s Web Pixel already reports checkout_started and checkout_completed for headless stores.

Option 1: the AgentShop pixel

Render once in your root layout. The AGENTSHOP_API_KEY you already set for SEO is the only configuration.
app/layout.tsx
It loads through Next’s <Script> with strategy="afterInteractive", the strategy Next documents for analytics.

What the script detects on its own

Every storefront-side standard event, with zero instrumentation: Cart quantities are deltas — what this event added or removed, not the line’s new total. Shopify merges a repeat add into the existing line, so reporting the total would count it twice. A cart(id:) query is also observed purely to establish the starting state, which is what lets a removal fire for an item added on a previous visit. Checkout events (checkout_startedcheckout_completed) come from our app’s Web Pixel on Shopify-hosted checkout — see the note above. Storefronts with non-standard routes can call the same events manually: window.AgentShopPixel("Search", { query }) and friends. On any other server-rendered stack, fetch the config and render the tag yourself:

Why not a pasted script tag

The pixel token identifies your store to our ingest endpoint, and it changes — reconnecting the app or uninstalling and reinstalling it issues a new one. A token pasted into your codebase is frozen at the moment you deployed. The day it changes, every pixel request returns 401 and attribution stops silently until someone notices and redeploys. <AgentShopPixel /> asks for the URL at render time and revalidates every 5 minutes, so a changed token is picked up on its own and no analytics credential is ever written into your source.

Option 2: forward Shopify’s analytics bus

If your storefront already runs Shopify’s analytics bus — <Analytics.Provider> in Hydrogen, or ShopifyScripts from the framework-agnostic Hydrogen SDK — forward that instead. You get Shopify’s typed event payloads, Shopify’s consent gating, and Shopify’s visitor identity rather than ours.
server
client
Do not call fetchPixelConfig() in browser code. It resolves its configuration from environment variables, which are absent there, so it returns null and the destination is silently never registered. Passing { apiKey } from the client instead would ship your store secret to every visitor — see Security.
On Hydrogen, hand the bus to subscribeAgentShop instead. Fetch pixel server-side (a loader, a server component) and pass it down as a prop — fetchPixelConfig() returns null when the store has no pixel configured or the endpoint is unreachable, so the component below no-ops until it has a real config:
app/components/AgentShopAnalytics.tsx

Events forwarded

page_viewed · product_viewed · collection_viewed · cart_viewed · search_viewed · product_added_to_cart · product_removed_from_cart cart_updated is deliberately not forwarded — it fires alongside the add and remove events for the same change, and reporting all three would triple-count one cart action.

What’s in event_data

event_data is an allowlist — exactly the flat projection the ingest endpoint reads: whichever of product_id, product_title, price, quantity, variant_id, handle, collection_id, search_term apply to that event type. A field is present only when Shopify’s bus payload actually carries it; nothing is null-filled, and the raw bus payload is deliberately not forwarded — Shopify’s payloads can carry full URLs and customer fragments the ingest never reads. On cart line events, quantity is the delta — how many this event added or removed — not the line’s post-update quantity. Shopify publishes both the previous and current line on an increment/decrement, and “added 1 more (now 3)” reporting as 3 would triple-count.

Prerequisite: the Storefront API proxy

uniqueToken and visitToken come from Server-Timing headers on Storefront API responses, which Shopify only exposes to a same-origin request at /api/<version>/graphql.json. If your storefront calls the Storefront API directly from the browser against *.myshopify.com, both values are empty and this destination sends nothing — deliberately. An event stamped with an identifier we invented is worse than a missing one, because it cannot be joined to the order that follows. Shopify stopped setting _shopify_y and _shopify_s on 1 January 2026 and deprecated them on 30 April 2026. The proxy is the supported replacement — see Shopify’s migration guide for custom headless builds, which also covers useShopifyCookies({ fetchTrackingValues: true }). Every event is gated on Shopify’s own answer:
That is the same call Hydrogen’s <Analytics.Provider> uses for its canTrack prop. It already encodes what varies by region — opt-in before tracking in some, opt-out in others — so this package never re-derives consent from a country code or a cookie. The check runs per event, because consent can be granted or withdrawn mid-session. The two paths differ on an unloaded API, and the difference matters.
Option 2 fails closed. No Customer Privacy API means no answer, and the analytics destination sends nothing. An unknown is not a yes.Option 1 fails open. The served script honours an explicit denial, but if the Customer Privacy API never loads it has nothing to consult and proceeds — writing cookies and sending events with no consent consulted. You must load the Customer Privacy API for consent to be honoured on that path at all.
Load it with useCustomerPrivacy() on Hydrogen, or loadFeatures on any other headless storefront:
Collect the visitor’s answer with setTrackingConsent, passing headlessStorefront: true. Pass a canTrack function to Option 2 if your storefront establishes consent some other way.

What Option 1 writes to the browser

Once it proceeds, the served script sets agentshop_llm (90 days, acquisition source), agentshop_cid (1 year, visitor id), agentshop_sid (24 hours, mirrored in sessionStorage), a localStorage visit counter (agentshop_vc), and — unless marketing consent is explicitly denied — Meta’s _fbp (1 year). These are the cookies ePrivacy Art. 5(3) consent rules govern, which is why the API has to be loaded.

What’s sent

Every event that passes the gate above also carries a consent object, built from the same Customer Privacy API:
marketingAllowed() answers marketing, adUserData, and adPersonalization together — Shopify exposes one marketing verdict, mapped onto all three ad-consent keys. saleOfData comes from saleOfDataAllowed(), unless the visitor’s browser sends Global Privacy Control (navigator.globalPrivacyControl) — a legally-binding Do-Not-Sell signal in effect in a number of US states since January 1, 2026. GPC forces saleOfData: false regardless of what Shopify’s API answers, and applies even when the Customer Privacy API isn’t loaded at all. A key is present only when Shopify (or GPC) has actually answered. An unanswered question is omitted, never sent as false — an omitted key can never overwrite a previously recorded consent answer downstream.
Both paths read ad-click identifiers for ad-attribution reporting — gclid, wbraid, gbraid, ttclid off the URL, fbc (from Meta’s _fbc cookie, or built from an fbclid per Meta’s spec), and fbp. An explicit marketing denial (marketingAllowed() returning false) withholds all of them — the served script sends them as null and skips writing the fbp cookie even if one exists; the analytics destination omits them entirely and never writes cookies at all. While consent is unanswered they flow normally, and the consent object above travels with the event for downstream enforcement.

Why you may see no events at all

Both paths report AI-referred visits, and that is all the dashboard shows. An event with no acquisition signal — no utm_source, no external referrer — still ships and is captured (the endpoint answers {"success": true, "attributed": false}), but it doesn’t surface as an AI-referred visit. Loading your own storefront directly is exactly that case, so a correct install can look broken. To verify, arrive with a source attached:

How this differs from the served script

The served <script> behind <AgentShopPixel /> keeps its own memory of how a visitor arrived: a last-non-direct model, storing the canonical source it resolved (chatgpt, perplexity, …) — not the raw UTM or referrer — in a 90-day cookie that spans sessions, overwritten each time a new source is detected. The analytics destination in Option 2 doesn’t: it reads UTM parameters and document.referrer fresh off the current page on every event, so an event fired several pages after the landing page may carry neither if the visitor navigated internally since. Delivery differs in one respect: both try navigator.sendBeacon first (no retry, ~64 KiB per call) and fall back — the destination to fetch({ keepalive: true }), the served script to XHR, which it retries up to 3 times on an HTTP error. So the real difference is retry-on-error, not the beacon path or a hard size ceiling.

What is and isn’t collected

IP address. The browser never sends one — but the ingest endpoint reads the connection IP, stores it on the visitor record, and passes it to Meta’s Conversions API as client_ip_address, which Meta uses for event matching. Meta’s spec requires that parameter to be sent unhashed, so it leaves as a raw address. IP is personal data under GDPR Art. 4(1) and that transfer is a separate processing purpose, so it belongs in your privacy policy. It is not gated by the marketing consent signal below — if you need it withheld, block the event. (The crawler endpoint is different: it accepts no IP field at all and derives its identifier from host + user agent.) Cookies and storage. Option 1’s served script writes them; Option 2’s destination writes none. See the cookie list. Everything else. No request bodies, and no headers beyond the user agent and referrer. The ad-click identifiers described above are the exception, and only when marketing consent hasn’t been explicitly denied — with one further carve-out: ga_client_id, read from your own _ga cookie, sits outside the marketing gate and is sent whenever analytics is allowed. No query strings or fragments either: both paths strip url and referrer to origin + pathname before anything leaves the browser. A magic-link or reset token in a query string never reaches analytics; the only query parameters attribution uses are the UTM keys and click identifiers, each extracted into its own allowlisted field. You send the raw signals a visit arrived with; attribution is resolved server-side. That keeps it accurate as AI platforms change how they tag outbound links, with no redeploy on your side.

Next steps

AI crawlers

Crawlers don’t run JavaScript. Report them from your server.

Endpoint reference

The pixel and event endpoints, with auth and response shapes.