Rokt Catalog SDK Guide for Headless Shopify Storefronts
This guide is for Rokt Catalog brands whose Shopify storefront is headless: a custom frontend such as Hydrogen, Next.js, or another React or Vue app built on the Storefront API.
On a standard Shopify theme, the Rokt Catalog app collects browsing activity automatically. A headless frontend doesn't run the Shopify online store, so the app can't see what shoppers do there. Installing the Rokt Web SDK on your frontend and sending three events (plus one optional) fills that gap and lets Rokt build your browsing audiences:
| Audience | Built from |
|---|---|
| All Site Visitors | page_viewed |
| Product Viewers | product_viewed |
| Add To Cart | product_added_to_cart |
The same events also build the "Not Purchased" audiences, which leave out shoppers who went on to buy. Each shopper lands in the "Not Purchased" audience for the furthest step they reached. For example, a shopper who added to cart is in "Add To Cart Not Purchased", not "Product Viewers Not Purchased".
You don't need to send checkout or purchase events. As long as the Rokt Catalog app is installed on your store, it collects checkout events from Shopify's hosted checkout, and purchases come from your order data.
Your Rokt account manager will provide your API key.
The instructions below require development resources. Test in a development environment before deploying to production.
1. Initialize the Rokt SDK1. Initialize the Rokt SDK への直接リンク
Load the SDK once, in the shared shell that renders on every page. Don't add it to a single route or component.
This is the initialization script:
// Enter the API key provided by your Rokt account manager
const API_KEY = "YOUR_API_KEY";
// Enter your custom subdomain if you are using a first-party domain configuration (optional)
const ROKT_DOMAIN = "https://apps.rokt-api.com";
window.mParticle = {
config: {
isDevelopmentMode: false,
},
};
// Load the SDK
(function(i){window.mParticle=window.mParticle||{};window.mParticle.EventType={Unknown:0,Navigation:1,Location:2,Search:3,Transaction:4,UserContent:5,UserPreference:6,Social:7,Other:8,Media:9};window.mParticle.eCommerce={Cart:{}};window.mParticle.Identity={};window.mParticle.Rokt={};window.mParticle.config=window.mParticle.config||{};window.mParticle.config.rq=[];window.mParticle.config.snippetVersion=3;window.mParticle.ready=function(e){window.mParticle.config.rq.push(e)};var e=["endSession","logError","logBaseEvent","logEvent","logForm","logLink","logPageView","setSessionAttribute","setAppName","setAppVersion","setOptOut","setPosition","startNewSession","startTrackingLocation","stopTrackingLocation"];var t=["setCurrencyCode"];var n=["identify","login","logout","modify","search"];var o=["selectPlacements","hashAttributes","hashSha256","setExtensionData","use","getVersion","terminate","onShoppableAdsReady"];e.forEach(function(e){window.mParticle[e]=r(e)});t.forEach(function(e){window.mParticle.eCommerce[e]=r(e,"eCommerce")});n.forEach(function(e){window.mParticle.Identity[e]=r(e,"Identity")});o.forEach(function(e){window.mParticle.Rokt[e]=r(e,"Rokt")});function r(t,n){return function(){if(n){t=n+"."+t}var e=Array.prototype.slice.call(arguments);e.unshift(t);window.mParticle.config.rq.push(e)}}var a=window.mParticle.config,c=a.isDevelopmentMode?1:0,s="?env="+c,w=a.dataPlan;if(w){var l=w.planId,m=w.planVersion;if(l){if(m&&(m<1||m>1e3)){m=null}s+="&plan_id="+l+(m?"&plan_version="+m:"")}}var d=a.versions;var p=[];if(d){Object.keys(d).forEach(function(e){p.push(e+"="+d[e])})}var f=document.createElement("script");f.type="text/javascript";f.async=true;window.ROKT_DOMAIN=ROKT_DOMAIN||"https://apps.rokt-api.com";window.mParticle.config.domain=window.ROKT_DOMAIN.split("//")[1];f.src=window.ROKT_DOMAIN+"/js/v3/"+i+"/app.js"+s+"&"+p.join("&");f.onerror=function(){var e=["https://apps.","rokt","ecommerce",".com"].join("");window.ROKT_DOMAIN=e;window.mParticle.config.domain=e.split("//")[1];var t=document.createElement("script");t.type="text/javascript";t.async=true;t.src=e+"/js/v3/"+i+"/app.js"+s+"&"+p.join("&");var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(t,n)};var u=document.getElementsByTagName("script")[0];u.parentNode.insertBefore(f,u)})(API_KEY);
Add it to your frontend as shown below. In the Next.js examples, ROKT_INIT is a string holding the script above.
Vite, Create React App, or another SPA: add it to index.html, inside <head>:
<script type="text/javascript">
/* initialization script */
</script>
Hydrogen (React Router-based, version 2026.4 or earlier): don't add the script as an inline <script> in <head>. The SDK inserts its own script tag next to it before React hydrates the page, which causes a React hydration error. Instead, run the initialization script inside an effect, in a component that renders first in the <body> of app/root.jsx. Paste the script as code, not as a string. Because this component renders before {children}, its effect runs before the effects in your routes, so window.mParticle is ready when they call it. Hydrogen 2026.10 and later is a different SDK, so these Hydrogen examples don't apply to it.
import {useEffect} from 'react';
function RoktSdk() {
useEffect(() => {
/* initialization script */
}, []);
return null;
}
export function Layout({children}) {
// ...existing Layout code...
return (
<html lang="en">
<head>{/* ...existing head... */}</head>
<body>
<RoktSdk />
{children}
{/* ...existing scripts... */}
</body>
</html>
);
}
Next.js (App Router): use next/script in app/layout.tsx. beforeInteractive makes window.mParticle available before your components run:
import Script from 'next/script';
const ROKT_INIT = `/* initialization script */`;
export default function RootLayout({children}: {children: React.ReactNode}) {
return (
<html lang="en">
<body>
{children}
<Script id="rokt-sdk" strategy="beforeInteractive" dangerouslySetInnerHTML={{__html: ROKT_INIT}} />
</body>
</html>
);
}
Next.js (Pages Router): add it inside <Head> in pages/_document.tsx:
<Head>
<script dangerouslySetInnerHTML={{__html: ROKT_INIT}} />
</Head>
To route the SDK through your own domain, see First Party Domain Integration.
2. Link browsing to checkout (recommended)2. Link browsing to checkout (recommended) への直接リンク
Shopify gives each visitor a unique token. The Rokt Catalog app sends this token from Shopify's checkout. Send the same token from your storefront so Rokt can connect a shopper's browsing to their purchase, even if they never log in. Without it, shoppers who buy without logging in can stay in your "Not Purchased" audiences.
Read the token with Shopify's getTrackingValues() and pass it to the SDK as other3. In Hydrogen, import it from @shopify/hydrogen. In other frontends, add the @shopify/hydrogen-react package and import it from there. Shopify's Customer Privacy API loads asynchronously, and the token can appear only after the shopper gives consent. So check again when consent changes, and briefly after the page loads. Run this once, in browser-only code, such as an effect in your root component:
import { getTrackingValues } from '@shopify/hydrogen-react'; // In Hydrogen: from '@shopify/hydrogen'
window.mParticle.ready(function () {
let linkedToken = '';
function linkShopifyVisitor() {
const { uniqueToken } = getTrackingValues();
if (!uniqueToken || uniqueToken === linkedToken) return;
linkedToken = uniqueToken;
// Keep identities the shopper already has, such as an email from step 4.
const user = window.mParticle.Identity.getCurrentUser();
const current = user ? user.getUserIdentities().userIdentities : {};
window.mParticle.Identity.identify({ userIdentities: { ...current, other3: uniqueToken } });
}
linkShopifyVisitor();
// Shopify publishes this event when the shopper's consent changes.
document.addEventListener('visitorConsentCollected', linkShopifyVisitor);
// Retry for a few seconds while Shopify's Customer Privacy API loads.
let attempts = 0;
const timer = setInterval(function () {
linkShopifyVisitor();
if (linkedToken || ++attempts >= 10) clearInterval(timer);
}, 1000);
});
getTrackingValues() only returns the right token when both of these are true:
- Your storefront loads Shopify's Customer Privacy API. Hydrogen's
<Analytics.Provider>loads it for you. - Your storefront requests Shopify data through your own domain, using a Storefront API proxy. Hydrogen's default request handler sets this up for you.
For other frontends, see Shopify's Migrating analytics tracking for custom headless builds. Otherwise the function can return an empty or outdated value that won't match checkout. Use the check in step 7 to confirm the token matches before you rely on it.
3. Send the browsing events3. Send the browsing events への直接リンク
Send each event with mParticle.logEvent, using the exact event names below and the event type mParticle.EventType.Other. Audiences only count events that match both.
Do not use mParticle.logPageView or the SDK's commerce events (eCommerce.logProductAction) for these. They are recorded differently and do not count toward Catalog audiences.
Page viewedPage viewed への直接リンク
Send on the first page load and on every client-side route change.
window.mParticle.ready(function () {
window.mParticle.logEvent('page_viewed', window.mParticle.EventType.Other);
});
Product viewedProduct viewed への直接リンク
Send from your product detail page component, once per product view.
window.mParticle.logEvent('product_viewed', window.mParticle.EventType.Other, {
productId: product.id, // e.g. "gid://shopify/Product/1234567890"
variantId: variant.id, // e.g. "gid://shopify/ProductVariant/1234567890"
sku: variant.sku,
price: variant.price.amount, // e.g. "48.00"
currency: variant.price.currencyCode, // e.g. "USD"
});
Collection viewed (optional)Collection viewed (optional) への直接リンク
Send from your collection or category page component.
window.mParticle.logEvent('collection_viewed', window.mParticle.EventType.Other);
Product added to cartProduct added to cart への直接リンク
Send from your add-to-cart handler.
window.mParticle.logEvent('product_added_to_cart', window.mParticle.EventType.Other, {
productId: product.id,
variantId: variant.id,
sku: variant.sku,
price: variant.price.amount,
currency: variant.price.currencyCode,
});
Pass the IDs and prices exactly as the Storefront API returns them. You don't need to transform them.
Call these from browser-only code. In server-rendered frameworks such as Next.js and Hydrogen, window doesn't exist during server rendering. In React, Next.js, Hydrogen, and Vue, send each event once per real shopper action, not on every render. Fire it inside an effect keyed to the product or collection ID, and guard against React Strict Mode running effects twice.
4. Identify the shopper4. Identify the shopper への直接リンク
When the shopper logs in or enters their email, identify them so their browsing links to their purchases. This lets Rokt leave existing customers out of your prospecting audiences.
Send the email together with the identities the shopper already has, so the visitor token from step 2 stays on their profile if you use it. This works whether or not you use step 2:
window.mParticle.ready(function () {
const user = window.mParticle.Identity.getCurrentUser();
const current = user ? user.getUserIdentities().userIdentities : {};
window.mParticle.Identity.identify({ userIdentities: { ...current, email: 'shopper@example.com' } });
});
For hashing options and more detail, see Identify the User.
5. Content Security Policy5. Content Security Policy への直接リンク
If your storefront sends a Content Security Policy header, allow the Rokt domains. Otherwise the browser will silently block events. See Content Security Policy Configuration.
Hydrogen sends a Content Security Policy by default, and that default blocks the Rokt SDK. Add the Rokt domains to createContentSecurityPolicy in app/entry.server.jsx. Setting scriptSrc or imgSrc replaces Hydrogen's default for that directive, so keep 'self' and https://cdn.shopify.com in both:
const {nonce, header, NonceProvider} = createContentSecurityPolicy({
// ...your existing options
scriptSrc: ["'self'", 'https://cdn.shopify.com', 'https://apps.rokt.com', 'https://apps.rokt-api.com', 'https://apps.roktecommerce.com'],
connectSrc: ['https://apps.rokt.com', 'https://apps.rokt-api.com', 'https://apps.roktecommerce.com'],
imgSrc: ["'self'", 'https://cdn.shopify.com', 'https://apps.rokt.com', 'https://*.mparticle.com'],
});
Keep any other domains your storefront already allows.
6. Consent6. Consent への直接リンク
Respect your shoppers' consent choices. If a shopper opts out of targeting or of the sale or sharing of their data, set the matching flags in the SDK's launcherOptions. See Cookie Consent Flags.
7. Test your integration7. Test your integration への直接リンク
- Load a page on your storefront with the browser's DevTools open.
- On the Network tab, filter for your
ROKT_DOMAINhost:apps.rokt-api.comby default, or your first-party subdomain if you configured one. - Confirm requests are sent when you load a page, open a product, and add an item to the cart.
- If you use step 2: note the
other3value in theidentifyrequest on your storefront, then go to checkout in the same browser. Theidentifyrequest at checkout should carry the sameother3value.
You can also use the Rokt SDK Helper to inspect the events.
Pre-launch checklistPre-launch checklist への直接リンク
- The SDK loads once, from the shared shell, with no CSP errors in the console.
page_viewedfires on the first load and on every route change.product_viewedandproduct_added_to_cartfire once per action, with the attributes above.- Every event uses
mParticle.EventType.Otherand the exact event names in this guide. - No
conversionevent is sent from the storefront. - The shopper is identified when their email becomes available.
- If you use step 2, the
other3token on your storefront matches the one at checkout. - Consent opt-outs set the SDK's consent flags.