This article explains how to integrate Iterable's Web SDK into Shopify's Hydrogen React framework. The concepts presented can be applied to any custom or headless storefront that utilizes Shopify's backend.
# In this article
# Overview
This article focuses on using client-side tracking to capture user interactions that you send directly to Iterable with the Web SDK. You can use the approach from this example to support additional use cases, events, and scenarios. Alternatively, if you prefer to manage event collection or user updates without using the SDK, you can achieve a similar outcome by calling Iterable’s REST APIs directly.
For server-side tracking that captures key Shopify events (such as customer creation and updates, checkouts, orders, and fulfillments) and product catalog data, which you can automatically send directly to Iterable, use Iterable’s official Shopify App, instead.
Or, if you prefer a combined client- and server-side tracking strategy that offers greater flexibility and coverage across your customer journey, install the app into your headless storefront.
# Prerequisites
Before you begin, add your Iterable API key to your Hydrogen environment variables. Then, install the Iterable Web SDK.
NOTE
These steps assume you're using a Web type key with JWT authentication enabled.
-
Add your Iterable API key to your Shopify environment variables.
ITERABLE_API_KEY=your_iterable_api_key
You can find your API key in your Iterable account under Settings > API Keys.
Install the Iterable Web SDK package, depending on your package manager.
npm install @iterable/web-sdk # or yarn add @iterable/web-sdk
# Step 1: Initialize and identify users with the Iterable SDK
The Iterable Context Provider initializes the SDK (once when your Hydrogen app loads) and automatically identifies Shopify customers who are logged in. This ensures that Iterable events are associated with the correct user profile throughout your app.
# 1.1 Create the Iterable context provider
Create a new app/context/IterableContext.jsx file that:
- Initializes the SDK once when the app loads.
- Identifies logged-in Shopify customers by Shopify customer ID (numeric format).
Alternatively, you can identify by email using
sdk.setEmail()instead ofsdk.setUserID(). - Uses the
isUserIdentifiedstate to track whether a user has been identified (viauserIdoremail). - Tracks events only when both
isInitializedandisUserIdentifiedare true. - Implements JWT token generation only for production use.
For example:
import {createContext, useContext, useEffect, useState, useRef} from 'react'; import {initializeWithConfig} from '@iterable/web-sdk'; const IterableContext = createContext(null); /** * JWT Token Generation Function * * ⚠️ Replace this with your own implementation. * In production, call your backend endpoint * that securely generates and returns a JWT token. * * Example (to replace below): * return fetch('/api/generate-jwt') * .then(res => res.json()) * .then(data => data.token); */ const fetchJwtToken = async () => '<your-jwt-token>'; export function IterableProvider({apiKey, customerData, children}) { const [iterableSDK, setIterableSDK] = useState(null); const [isInitialized, setIsInitialized] = useState(false); const [isUserIdentified, setIsUserIdentified] = useState(false); const initializingRef = useRef(false); useEffect(() => { // Guard against re-initialization if (initializingRef.current) return; if (!apiKey) return; const initAndIdentify = async () => { try { initializingRef.current = true; console.log('🚀 Initializing Iterable SDK...'); // Initialize SDK const sdk = initializeWithConfig({ authToken: apiKey, configOptions: { isEuIterableService: false, // Set to true if using EU data center dangerouslyAllowJsPopups: true, // Enable for in-app messages }, generateJWT: fetchJwtToken, }); setIterableSDK(sdk); setIsInitialized(true); console.log('✅ Iterable SDK initialized'); // Identify user if logged in if (customerData?.id) { // Extract numeric ID from Shopify's GraphQL ID format // Example: "gid://shopify/Customer/1234567890" -> "1234567890" const customerId = customerData.id.substring( customerData.id.lastIndexOf('/') + 1, ); // Identify by user ID (recommended) await sdk.setUserID(customerId); setIsUserIdentified(true); console.log('✅ Iterable User ID set:', customerId); // Alternative: Identify by email // await sdk.setEmail(customerData.email); // setIsUserIdentified(true); // console.log('✅ Iterable Email set:', customerData.email); } else { setIsUserIdentified(false); } } catch (err) { console.error('❌ Error initializing Iterable SDK:', err); setIsInitialized(false); setIsUserIdentified(false); } }; initAndIdentify(); }, [apiKey, customerData?.id]); return ( <IterableContext.Provider value={{iterableSDK, isInitialized, isUserIdentified}} > {children} </IterableContext.Provider> ); } export function useIterableContext() { const context = useContext(IterableContext); if (context === undefined) { throw new Error( 'useIterableContext must be used within an IterableProvider', ); } return context; }
# 1.2 Integrate the provider into your root layout
Update app/root.jsx to enable the Iterable SDK available across your entire app.
Add imports:
import {IterableProvider} from './context/IterableContext'; import {CUSTOMER_DETAILS_QUERY} from './graphql/customer-account/CustomerDetailsQuery';
Update the loader function:
export async function loader(args) { // Start fetching non-critical data without blocking time to first byte const deferredData = loadDeferredData(args); // Await the critical data required to render initial state of the page const criticalData = await loadCriticalData(args); const {storefront, env} = args.context; // Fetch customer data if user is logged in const isLoggedIn = await deferredData.isLoggedIn; let customerData; if (isLoggedIn) { const {data, errors} = await args.context.customerAccount.query( CUSTOMER_DETAILS_QUERY, ); customerData = data.customer; } else { customerData = {}; } return { iterableApiKey: env.ITERABLE_API_KEY, // Add this customerData, // Add this ...deferredData, ...criticalData, publicStoreDomain: env.PUBLIC_STORE_DOMAIN, shop: getShopAnalytics({ storefront, publicStorefrontId: env.PUBLIC_STOREFRONT_ID, }), consent: { checkoutDomain: env.PUBLIC_CHECKOUT_DOMAIN, storefrontAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN, withPrivacyBanner: false, country: args.context.storefront.i18n.country, language: args.context.storefront.i18n.language, }, }; }
WARNING
Content security policies (usually located in the entry.server.jsx Hydrogen
file) can impact the way Iterable scripts work in both local and production
environments. Before deploying your app, throughly test preview builds that you
send to Shopify through Oxygen or custom deployments. If you encounter issues,
configure your CSP to allow our JavaScript to function.
Wrap your app with the provider:
export function Layout({children}) { const nonce = useNonce(); const data = useRouteLoaderData('root'); return ( <html lang="en"> <head> <meta charSet="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <Meta /> <Links /> </head> <body> {data ? ( <Analytics.Provider cart={data.cart} shop={data.shop} consent={data.consent} > <IterableProvider apiKey={data.iterableApiKey} customerData={data.customerData} > <PageLayout {...data}>{children}</PageLayout> </IterableProvider> </Analytics.Provider> ) : ( children )} <ScrollRestoration nonce={nonce} /> <Scripts nonce={nonce} /> </body> </html> ); }
NOTE
Place IterableProvider inside Analytics.Provider but outside PageLayout.
At this stage, logged-in Shopify users will be identified in the Iterable SDK
using their Shopify userId — or email, if you’ve chosen that as your identifier.
# Step 2: Track custom events (such as product views)
Learn how to track a custom event with the Iterable Web SDK. In this example, you’ll capture product view events, which can help identify user interest and enable browse-based personalization.
# 2.1 Create a product view tracking hook
Create a new file app/hooks/useIterableEvents.js.
This hook uses the Iterable SDK context to track when a customer views a product
and can be easily extended to support other event types.
import {useIterableContext} from '~/context/IterableContext'; import {track} from '@iterable/web-sdk'; export function useIterableEvents() { const {isInitialized, isUserIdentified} = useIterableContext(); /** * Track product view events * @param {Object} product - Product data from Shopify * @param {string} url - Current page URL */ const trackProductView = async (product, url) => { if (!isInitialized) { console.log( 'Iterable SDK not initialized, skipping product view tracking', ); return; } if (!isUserIdentified) { console.log( 'User not identified in Iterable SDK, skipping product view tracking', ); return; } try { const variant = product.selectedOrFirstAvailableVariant; const dataFields = { product_id: product.id.split('/').pop(), product_name: product.title, variant_id: variant.id.split('/').pop(), image_url: variant.image?.url, price: parseFloat(variant.price.amount), currency: variant.price.currencyCode, sku: variant.sku, url, }; await track({eventName: 'Product Viewed', dataFields}); console.log('✅ Successfully tracked product view for:', product.title); return true; } catch (error) { console.error('❌ Error tracking product view:', error); return false; } }; return { trackProductView, }; }
# 2.2 Add product view tracking to product page
Update your product page route (typically app/routes/products.$handle.jsx):
Add imports:
import {useEffect} from 'react'; import {useIterableEvents} from '~/hooks/useIterableEvents';
Track product views:
export default function Product() { const {product} = useLoaderData(); const {trackProductView} = useIterableEvents(); useEffect(() => { trackProductView(product, window.location.href); }, []); // ... rest of your component }
# Step 3: Track cart updates
Synchronize cart activity with Iterable in real time for abandoned-cart and conversion tracking.
# 3.1 Extend the event hook for cart tracking
Update app/hooks/useIterableEvents.js to include cart tracking functionality.
Update imports:
import {useEffect, useRef} from 'react'; import {useIterableContext} from '~/context/IterableContext'; import {track, updateCart} from '@iterable/web-sdk';
Add the cart tracking function inside useIterableEvents:
/** * Update cart in Iterable * @param {Object} cart - Cart data from Shopify */ const trackCartUpdate = async (cart) => { if (!isInitialized) { console.log('Iterable SDK not initialized, skipping cart update'); return; } if (!isUserIdentified) { console.log('User not identified in Iterable SDK, skipping cart update'); return; } if (!cart) return; try { const formattedItems = cart.lines.nodes.map((item) => ({ id: item.merchandise.id, name: item.merchandise.product.title, quantity: item.quantity, price: parseFloat(item.merchandise.price.amount), sku: item.merchandise.sku || '', imageUrl: item.merchandise.image?.url, url: `/products/${item.merchandise.product.handle}`, })); await updateCart({items: formattedItems}); console.log('✅ Successfully updated cart in Iterable'); return true; } catch (error) { console.error('❌ Error updating cart in Iterable:', error); return false; } };
Update the return statement:
return { trackProductView, trackCartUpdate };
# 3.2 Automatically track cart changes
Add this new hook to the end of the same file:
/** * Automatically tracks cart changes to Iterable * Only fires events when cart contents actually change * @param {Object} cart - The cart object from Shopify */ export function useCartTracking(cart) { const {trackCartUpdate} = useIterableEvents(); const previousCartRef = useRef(null); useEffect(() => { // Skip if no cart data if (!cart) return; // Create a stable identifier for the current cart state const currentCartSignature = JSON.stringify({ id: cart.id, totalQuantity: cart.totalQuantity, lines: cart.lines?.nodes?.map((line) => ({ id: line.merchandise.id, merchandiseId: line.merchandise?.id, quantity: line.quantity, })) || [], }); // Only track if cart state has actually changed if (previousCartRef.current !== currentCartSignature) { // Update only after initial mount (skip first render) if (previousCartRef.current !== null) { trackCartUpdate(cart); } previousCartRef.current = currentCartSignature; } }, [cart, trackCartUpdate]); }
# 3.3 Add cart tracking to your cart component
In app/components/CartMain.jsx:
import {useCartTracking} from '~/hooks/useIterableEvents'; export function CartMain({layout, cart: originalCart}) { const cart = useOptimisticCart(originalCart); // Automatically track cart changes useCartTracking(cart); // ... rest of your component }
After this step, any cart modifications (add, remove, or quantity change) are automatically synced with Iterable in real time.
# Step 4 (Optional): Set up campaign attribution
If you’re using the Iterable Shopify App, you can automatically attribute purchases to Iterable campaigns by passing campaign identifiers through cart attributes. This allows the app to link orders to the campaign that drove the purchase when the order webhook fires.
NOTE
The following implementation is one example. Your setup may differ based on your
storefront architecture or URL parameter naming. Confirm which parameters (e.g.
iterable_campaign, iterable_template, or utm_*) your team uses for
tracking.
# Example implementation
When a customer clicks through from an Iterable campaign and makes a purchase, you want that purchase attributed back to the campaign. Here's how it works:
- Customer clicks an Iterable campaign link → Lands on your site with
?iterable_campaign=123. - URL parameters are stored in cookies (for 24 hours).
- When the customer adds to cart, attribution parameters are merged into cart attributes.
- The order inherits those attributes at checkout.
- The Iterable Shopify App reads them via webhook → purchase is attributed to the campaign. 🎉
Required cart attributes:
-
__iterable_campaign_id- Iterable campaign ID -
__iterable_template_id- Iterable template ID
These exact names are required for attribution in the Iterable Shopify App.
# 4.1: Create attribution utilities
Create app/lib/attribution.js:
// Maps URL parameter names to cookie names const PARAM_MAPPINGS = { iterable_campaign: 'iterable_campaign_id', iterable_template: 'iterable_template_id', }; /** * Saves Iterable parameters from URL to cookies (expires in 24 hours) * @param {Object} params - URL parameters object (e.g., from URLSearchParams) */ export function setAttributionCookies(params) { if (typeof document === 'undefined') return; Object.entries(PARAM_MAPPINGS).forEach(([urlParam, cookieName]) => { if (params[urlParam]) { document.cookie = `${cookieName}=${params[urlParam]}; path=/; max-age=86400`; } }); } /** * Reads attribution from server-side request cookies * @param {string} cookieHeader - Cookie header from request * @returns {Object} Object with iterable_campaign_id and iterable_template_id */ export function getAttributionFromRequest(cookieHeader) { if (!cookieHeader) return {}; const cookies = cookieHeader.split(';').reduce((acc, cookie) => { const [name, value] = cookie.trim().split('='); if (name && value) acc[name] = decodeURIComponent(value); return acc; }, {}); return { iterable_campaign_id: cookies.iterable_campaign_id, iterable_template_id: cookies.iterable_template_id, }; } /** * Merges Iterable attribution with existing cart attributes * Replaces any existing Iterable attributes, preserves all others * @param {Array} existingAttributes - Current cart attributes * @param {Object} attribution - Attribution object with campaign/template IDs * @returns {Array} Merged attributes array */ export function mergeAttributionWithCart( existingAttributes = [], attribution = {}, ) { // Keep all non-Iterable attributes const preserved = existingAttributes.filter( (attr) => !attr.key.startsWith('__iterable_'), ); // Build new Iterable attributes const newAttrs = []; if (attribution.iterable_campaign_id) { newAttrs.push({ key: '__iterable_campaign_id', value: attribution.iterable_campaign_id, }); } if (attribution.iterable_template_id) { newAttrs.push({ key: '__iterable_template_id', value: attribution.iterable_template_id, }); } return [...preserved, ...newAttrs]; }
# 4.2 Capture URL parameters
In app/root.jsx, set cookies on page load:
import {useEffect} from 'react'; import {setAttributionCookies} from '~/lib/attribution'; export function Layout({children}) { // ... existing code ... useEffect(() => { const params = new URLSearchParams(window.location.search); setAttributionCookies(Object.fromEntries(params.entries())); }, []); // ... rest of layout ... }
# 4.3 Update cart actions
In app/routes/cart.jsx, merge attribution values into cart attributes:
import { getAttributionFromRequest, mergeAttributionWithCart, } from '~/lib/attribution'; export async function action({request, context}) { // ... existing cart action logic ... if (result?.cart) { const attribution = getAttributionFromRequest( request.headers.get('Cookie'), ); if (attribution.iterable_campaign_id || attribution.iterable_template_id) { const mergedAttributes = mergeAttributionWithCart( result.cart.attributes || [], attribution, ); result = await cart.updateAttributes(mergedAttributes); } } // ... rest of action ... }
# Test and verify your integration
IMPORTANT
User identification is required for event tracking. In order to properly track events such as product views and cart updates, users must be identified in the Iterable SDK. This means:
- Users must be logged into your Shopify store
- The SDK must successfully call
setUserID()orsetEmail()
# Verify SDK initialization
Open your browser's developer console and look for after logging in to your storefront:
🚀 Initializing Iterable SDK... ✅ Iterable SDK initialized ✅ Iterable User ID set: [customer_id]
# Test product view tracking
- Log in to your store account
- Navigate to any product page
- Check the console for:
✅ Successfully tracked product view for: [Product Name]
- Check the corresponding Iterable user profile to ensure the
Product Viewedevent was tracked to their profile.
# Test cart tracking
- Add an item to your cart
- Check the console for:
✅ Successfully updated cart in Iterable
- Change item quantity or remove items
- Verify each change is tracked in the console
- Check the corresponding Iterable user profile to ensure the
updateCartevent was tracked to their profile.
# Test attribution
Test with: https://yourstore.com?iterable_campaign=test123&iterable_template=test456
Add an item to cart and check the cart response for __iterable_campaign_id and
__iterable_template_id attributes. If you're using the Iterable Shopify App to
track checkout or purchase events, verify that the campaignId and templateId
fields appear as expected in the events sent from the app to Iterable.