Custom / headless storefronts & Device Intelligence setup

Updated on 23.06.26
1 minute to read
Copy link

Overview

Hydrogen, Next.js, Remix, Nuxt, and similar frameworks

On a headless storefront there is no Shopify theme, so the SEON theme app extension cannot run.

Instead, you embed the SEON browser agent in your storefront, generate an encrypted session payload, and store it in localStorage under seon_visitor_id. The SEON app's web pixel, installed automatically with the app, reads that value on checkout_completed and submits it. SEON then correlates it to the resulting order by cart token.

The example below is for Hydrogen using React Router / Oxygen. Other frameworks follow the same pattern. See other frameworks at the end.

How it works

  1. A client-only component loads the SEON SDK and calls seon.getSession() to produce the encrypted payload.
  2. It stores the payload in localStorage['seon_visitor_id'] and tracks the cart token in localStorage['seon_cart_token'], regenerating the payload whenever the cart changes.
  3. On checkout completion, the SEON web pixel reads seon_visitor_id and submits it.
  4. SEON's fraud pipeline matches the payload to the order and attaches the device intelligence.

Prerequisites

Same-origin checkout required. This integration relies on localStorage, which is scoped to the origin. The storefront and checkout must share the same origin for the web pixel to read the fingerprint.

SetupWorks
yourstore.com – yourstore.com/checkoutYes
yourstore.com – checkout.yourstore.comNo

If your checkout runs on a different subdomain from your storefront, contact SEON support — a different integration approach is required.

Other prerequisites:

  • The SEON Shopify app installed and connected for your shop. This provisions the web pixel that submits the fingerprint.
  • A Hydrogen app with <Analytics.Provider> in root.tsx. This is present by default in the Hydrogen skeleton.

Step 1 — Add the component

Create app/components/SeonFingerprint.tsx:

import {useEffect, useRef} from 'react';
import {useAnalytics} from '@shopify/hydrogen';

/**
 * SEON device-fingerprinting for the storefront.
 *
 * Loads the SEON SDK, generates an encrypted session via seon.getSession(),
 * and stores it in localStorage['seon_visitor_id'] — the key the installed
 * SEON web pixel reads on checkout_completed.
 *
 * Regenerated whenever the cart changes.
 *
 * Must be rendered inside <Analytics.Provider>.
 */

const SEON_ENV_KEY = 'seon_env';
const ENV_DEV = 'development';
const ENV_PROD = 'production';
const STORAGE_CART_TOKEN = 'seon_cart_token';
const STORAGE_VISITOR_ID = 'seon_visitor_id';

type SeonSessionConfig = {
  dnsResolverDomain: string;
  fieldTimeoutMs: number;
  geolocation: {canPrompt: boolean};
  networkTimeoutMs: number;
  region: string;
  silentMode: boolean;
};

declare global {
  interface Window {
    SEON_ENV?: string;
    seon?: {
      init?: () => void;
      getSession?: (config: SeonSessionConfig) => Promise<string>;
    };
  }
}

function getStoredEnv(): string | null {
  try {
    return sessionStorage.getItem(SEON_ENV_KEY);
  } catch {
    return null;
  }
}

function setStoredEnv(value: string): void {
  try {
    sessionStorage.setItem(SEON_ENV_KEY, value);
  } catch {
    // Avoid logging in customer browser.
  }
}

function getSeonEnv(): string {
  const urlEnv = new URLSearchParams(window.location.search).get('seon_env');

  if (urlEnv === ENV_DEV || urlEnv === ENV_PROD) {
    setStoredEnv(urlEnv);
    return urlEnv;
  }

  const stored = getStoredEnv();
  if (stored !== null) return stored;

  return window.SEON_ENV ?? ENV_PROD;
}

function isSeonDevelopment(): boolean {
  return getSeonEnv() === ENV_DEV;
}

function buildSeonSessionConfig(): SeonSessionConfig {
  const dnsResolverDomain = isSeonDevelopment()
    ? 'seondnsresolve.com'
    : 'seonintelligenceresolver.com';

  return {
    dnsResolverDomain,
    fieldTimeoutMs: 2000,
    geolocation: {canPrompt: false},
    networkTimeoutMs: 2000,
    region: 'eu', // Set to your SEON account region. See "Set your region".
    silentMode: true,
  };
}

function getSeonSdkUrl(): string {
  const prodUrl = 'https://cdn.seonintelligence.com/js/v6/agent.umd.js';
  const devUrl = 'https://cdn.seon.io/js/v6/agent.dev.umd.js';

  return isSeonDevelopment() ? devUrl : prodUrl;
}

async function seonPayload(token: string | null): Promise<void> {
  if (!token) return;

  const sdk = window.seon;
  if (!sdk || typeof sdk.getSession !== 'function') return;

  try {
    const data = await sdk.getSession(buildSeonSessionConfig());

    if (typeof data === 'string' && data.trim().length > 0) {
      localStorage.setItem(STORAGE_VISITOR_ID, data);
    }
  } catch {
    // Storefront: avoid logging in customer browser.
  }
}

async function ensureFingerprint(cartToken: string | null): Promise<void> {
  const hasVisitorId = Boolean(localStorage.getItem(STORAGE_VISITOR_ID));

  if (cartToken) {
    const storedToken = localStorage.getItem(STORAGE_CART_TOKEN);
    const tokenChanged = storedToken !== cartToken;

    if (tokenChanged) {
      localStorage.setItem(STORAGE_CART_TOKEN, cartToken);
      localStorage.removeItem(STORAGE_VISITOR_ID);
    }

    if (tokenChanged || !hasVisitorId) {
      await seonPayload(cartToken);
    }

    return;
  }

  if (!hasVisitorId) {
    await seonPayload(crypto.randomUUID());
  }
}

export function SeonFingerprint() {
  const {cart} = useAnalytics();
  const cartId = cart?.id ?? null;

  const injected = useRef(false);
  const sdkReady = useRef(false);
  const cartIdRef = useRef<string | null>(cartId);

  cartIdRef.current = cartId;

  // Inject the SEON SDK once on mount.
  useEffect(() => {
    if (injected.current) return;

    injected.current = true;

    try {
      const src = getSeonSdkUrl();
      const existing = document.querySelector<HTMLScriptElement>(
        `script[data-seon-sdk="${src}"]`,
      );

      if (existing) {
        sdkReady.current = true;
        void ensureFingerprint(cartIdRef.current);
        return;
      }

      const script = document.createElement('script');

      script.src = src;
      script.defer = true;
      script.dataset.seonSdk = src;
      script.setAttribute('fetchpriority', 'low');

      script.onload = () => {
        sdkReady.current = true;
        window.seon?.init?.();
        void ensureFingerprint(cartIdRef.current);
      };

      document.head.appendChild(script);
    } catch {
      // Storefront: avoid logging in customer browser.
    }
  }, []);

  // Regenerate when the cart changes, once the SDK is ready.
  useEffect(() => {
    if (!sdkReady.current) return;

    void ensureFingerprint(cartId);
  }, [cartId]);

  return null;
}

The component renders nothing. It only runs effects on the client, so it is safe under Hydrogen's server rendering.

Step 2 — Render it inside Analytics.Provider

In app/root.tsx, render <SeonFingerprint /> inside the existing provider so useAnalytics() can supply the cart:

import {SeonFingerprint} from './components/SeonFingerprint';

// ...

return (
  <Analytics.Provider cart={data.cart} shop={data.shop} consent={data.consent}>
    <SeonFingerprint />

    <PageLayout {...data}>
      <Outlet />
    </PageLayout>
  </Analytics.Provider>
);

Step 3 — Allow SEON in your Content Security Policy

Hydrogen ships a strict CSP via createContentSecurityPolicy. Without the required SEON entries, the CSP blocks the SDK and its network calls.

In app/entry.server.tsx:

const {nonce, header, NonceProvider} = createContentSecurityPolicy({
  shop: {
    checkoutDomain: context.env.PUBLIC_CHECKOUT_DOMAIN,
    storeDomain: context.env.PUBLIC_STORE_DOMAIN,
  },

  // SEON device-fingerprinting SDK.
  scriptSrc: [
    "'self'",
    'https://cdn.seonintelligence.com',
    'https://cdn.seon.io',
  ],

  connectSrc: [
    "'self'",
    'https://*.seonintelligenceresolver.com',
    'https://*.seondnsresolve.com',
    'https://*.seonintelligence.com',
    'https://*.seon.io',
  ],
});

Set your region

In buildSeonSessionConfig(), set region to match your SEON account region:

region: 'eu', // or 'us'

If the region is set incorrectly, the fingerprint will not resolve.

Development mode

Append ?seon_env=development to any storefront URL to load the development SDK (cdn.seon.io/js/v6/agent.dev.umd.js). The choice persists in sessionStorage for the tab. Append ?seon_env=production to switch back. The default is production.

Verifying

  1. Load a storefront page.
  2. Open DevTools – Application – Local Storage.
  3. Confirm seon_cart_token is present.
  4. Add an item to the cart and confirm both seon_cart_token and seon_visitor_id update.
  5. In the Network tab, confirm agent.umd.js loads with no CSP violations.
  6. Place a test order and confirm device intelligence appears on the order in SEON.

Other frameworks

The pattern is the same across frameworks. Only the mounting differs.

  • Load the SDK on the client side.
  • Call seon.init(), then seon.getSession().
  • Store the result in localStorage['seon_visitor_id'].
  • Track the cart token in localStorage['seon_cart_token'] and regenerate the payload when it changes.
  • Mount the logic client-only — for example with a useEffect hook in React, or next/dynamic in Next.js.
  • Add the SEON CDN and connect domains to your CSP.

The same-origin requirement applies regardless of framework. Verify that your storefront and checkout share the same origin before integrating.