Install Apex on a headless React storefront
Add Apex to a Next.js or Hydrogen storefront, render experiment branches with React hooks, and preserve Shopify cart attribution.
For headless React storefronts, Apex still starts with the global SDK snippet. The snippet handles anti-flicker protection, assignment, persistence, pageview tracking, and event delivery before your React tree finishes rendering. Use the npm React bindings when a test needs native branch rendering for cart logic, product modules, or state-coupled UI. Apex owns the assignment and stats; your components only read the active variation or flag.
Add the script to your app shell
Place the shop-specific snippet once in the root document head, such as Next.js app/layout.tsx or the Hydrogen app shell. The preconnect starts the Worker connection before the browser discovers the script.
<link rel="preconnect" href="<worker-origin>" crossorigin>
<script src="<worker>/s/<shopId>.js"></script>The synchronous tag above is the safe default: it arms the anti-flicker guard before first paint and has assignment plus goal tracking active before the page is interactive. An async variant exists as an explicit opt-in for after-hydration stores with anti-flicker off that treat render blocking as a hard constraint — it does not change flicker behavior there, but the page can become interactive before assignment and tracking are ready, so early shopper actions may be missed or mis-attributed:
<!-- Explicit opt-in only: after-hydration store, anti-flicker off, measurement trade-off accepted -->
<link rel="preconnect" href="<worker-origin>" crossorigin>
<script src="<worker>/s/<shopId>.js" async></script>Keep synchronous loading whenever anti-flicker is scoped or full — an async tag arms the guard only after the script arrives — for immediate-apply themes, and whenever conversion attribution matters more than the ~250–400 ms render-blocking cost (it usually does).
Headless / strict CSP checklist
For script-src 'strict-dynamic' 'nonce-…' storefronts:
- Render the shop-specific loader with the page's fresh server-generated nonce:
<script nonce="<per-request-nonce>" src="<worker>/s/<shopId>.js"></script>. Asdk.drip-apex.comhost allowlist is not a substitute understrict-dynamic. - Add
https://events.drip-apex.comto the existingconnect-srcdirective. This client-owned one-line CSP change is mandatory for event POSTs. - Keep the existing
style-src 'unsafe-inline'allowance if Apex anti-flicker is enabled; the anti-flicker style needs no Apex-side change. - Verify a pageview and conversion request in browser developer tools after deployment. Live CSP verification is a downstream client/staging check, not something the package build can certify.
The Worker-served snippet copies its nonce to a secondary SDK tag when the configured SDK-delivery policy creates one. Other scripts created by that trusted SDK inherit trust under strict-dynamic. For an npm install, the SDK is compiled into the storefront's application bundle, so authorize the application bundle through the site's existing CSP path; there is no Apex loader tag to nonce. GTM is an alternative only when the GTM bootstrap is already nonced: install Apex from that trusted container, accepting that tag-manager timing cannot provide the direct head tag's early anti-flicker guarantee.
The Worker's x-drip-csp-nonce edge-injection support is not involved when Oxygen or another headless platform fronts and serves the HTML. In that topology the application must put the nonce on the Apex tag itself.
SPA navigation
The SDK observes browser history changes, popstate, hashchange, and the Navigation API automatically. Install the snippet in the global shell rather than adding it per route. For React route transitions that re-render product or cart modules, set routerMode to history, navigation, or auto and enable spa.domChangeTrigger when Apex should re-assert eligible changes after DOM updates.
Consent tools
Apex integrates with OneTrust, Cookiebot, Usercentrics, CCM19, Pandectes, and CookieScript through consentMode: { enabled: true, provider: "..." }. For any other CMP, use the custom provider with a getter that returns whether tracking consent is currently granted. Keep Apex behind the same consent policy you apply to testing and analytics scripts, then run a granted-consent test visit during verification.
Branch rendering in React (npm package)
Install @drip-apex/sdk, wrap your app in DripProvider, and read useVariation(experimentId) or useFeatureFlag(flagId) inside components. This is the right path for tests that need native React state, cart behavior, or module-scoped rendering while Apex handles assignment, persistence, and stats. See the React SDK reference for every provider prop and hook.
import { DripProvider, useFeatureFlag, useVariation } from "@drip-apex/sdk/react";
export function StorefrontProviders({ children }: { children: React.ReactNode }) {
return (
<DripProvider shopId="<shopId>" endpoint="https://events.drip-apex.com">
{children}
</DripProvider>
);
}
function ProductOffer() {
const trustBadge = useVariation("product-trust-badge");
const showBundle = useFeatureFlag("bundle-module");
return (
<>
{trustBadge?.id === "instant-access" ? <InstantAccessBadge /> : null}
{showBundle ? <BundleUpsell /> : <DefaultProductDetails />}
</>
);
}Purchase attribution with Storefront API carts
Checkout attribution on a shared registrable domain
If Shopify's analytics bus is unavailable, Apex writes the current assignment sync to a first-party cookie after tracking consent is granted. A checkout on the same registrable domain can read the cookie through the Apex web pixel and attribute purchase events. For example, shop.example.com and checkout.example.com can share the cookie.
The cookie cannot cross registrable domains. A custom storefront and a checkout on myshopify.com do not share it. Keep the cart-attribute integration below as the webhook-attribution hardening path. It is required for every charge-affecting price, shipping, or offer experiment.
Headless carts must carry Apex attribution into Shopify. Because Shopify's cartAttributesUpdate mutation replaces the complete attributes array, load the cart's current attributes and pass them to window.drip.mergeCartAttributes() before updating. The method normalizes valid existing attributes to Array<{key, value}>, replaces only keys present in the fresh Apex stamp, and preserves all other attributes. When Apex has no fresh stamp, existing attribution remains unchanged. Re-sync after cartCreate and every cart mutation because assignments can arrive after first paint. Orders can then attribute server-side through Shopify order webhooks.
Charge-affecting experiments use the signed flow instead. If the shop runs price, shipping, or offers experiments (anything where the treatment changes what the customer is charged), the cart's apex_assignment_authorization must come from window.drip.getSignedCartAttributes(cartGid) — write every entry it returns to the cart and call confirmCartAttributesWritten(cartGid) after the mutation succeeds with no userErrors. Do not follow a signed write with an unsigned mergeCartAttributes() re-sync on the same cart: the unsigned stamp replaces the signed authorization, and commerce authorization then fails closed, suppressing the treatment at checkout. Use mergeCartAttributes() for standard (non-charge-affecting) attribution only.
const { cart } = await storefrontClient.query(CART_ATTRIBUTES_QUERY, {
variables: { cartId },
});
const attributes = window.drip?.mergeCartAttributes?.(cart?.attributes) ?? cart?.attributes ?? [];
await storefrontClient.mutate(CART_ATTRIBUTES_UPDATE, {
variables: {
cartId,
attributes,
},
});Optional cart-metafield mirror for BI
Headless shops can opt in to mirroring the same Apex key/value stamp into Shopify cart metafields. This mirror is advisory only. Never use it as attribution truth. The signed cart-attribute flow documented above remains the authoritative attribution plane.
Required order-metafield setup
Shopify copies a cart metafield to an order only when a matching order metafield definition exists. The definition must use the same namespace and key. It must also enable the cartToOrderCopyable capability. This capability requires Shopify Admin API version 2026-04 or later.
Create one ORDER definition for each mirrored key. The default namespace is apex.
| Key | Type |
|---|---|
apex_assignments | json |
apex_assignment_authorization | single_line_text_field |
apex_experiment_id | single_line_text_field |
apex_variation_id | single_line_text_field |
apex_assignment_epoch | single_line_text_field |
apex_visitor_id | single_line_text_field |
apex_session_id | single_line_text_field |
The following Admin GraphQL mutation creates the definition for apex_assignments. Run it against API version 2026-04 or later. Repeat it for each key in the table. Use single_line_text_field for the other keys.
mutation CreateApexAssignmentsOrderMetafield {
metafieldDefinitionCreate(
definition: {
name: "Apex assignments"
namespace: "apex"
key: "apex_assignments"
ownerType: ORDER
type: "json"
capabilities: { cartToOrderCopyable: { enabled: true } }
}
) {
createdDefinition { id namespace key type { name } }
userErrors { field message code }
}
}After these definitions exist, Shopify can copy matching cart metafields to the completed order. BI and extract, load, transform tools can then ingest the experiment context.
Write the current mirror
const metafields = window.drip?.getCartMetafields?.() ?? [];
await storefrontClient.mutate(`
mutation SetApexCartMetafields($metafields: [CartMetafieldsSetInput!]!) {
cartMetafieldsSet(metafields: $metafields) {
metafields { key namespace type value }
userErrors { field message }
}
}
`, {
variables: {
metafields: metafields.map((metafield) => ({ ...metafield, ownerId: cartId })),
},
});The returned keys are composite Shopify keys such as apex.apex_assignments. Each object can be spread with ownerId as shown above. This helper is headless-only because the Storefront API is Shopify's cart-metafield write path. Liquid and theme storefronts cannot write this mirror.
getCartMetafields() omits empty values. Apex cart attributes use empty values to clear prior treatment stamps. cartMetafieldsSet only upserts the supplied keys, so an omitted key can retain a stale value on the cart. For optional cleanup, delete the complement before or after the upsert. Use the same namespace option for both helpers.
const CART_METAFIELD_DELETE = `mutation DeleteStaleApexCartMetafield($input: CartMetafieldDeleteInput!) { cartMetafieldDelete(input: $input) { deletedId userErrors { field message } } }`;
await Promise.all((window.drip?.getCartMetafieldStaleKeys?.() ?? []).map((key) => storefrontClient.mutate(CART_METAFIELD_DELETE, { variables: { input: { ownerId: cartId, key } } })));See the getCartMetafields runtime API reference for the return shape and namespace option.
Confirm events arrive
- Publish the snippet to the headless storefront.
- Open the live storefront and visit a tracked page.
- If consent is required, grant consent in the banner during the test visit.
- Return to Apex onboarding or Installation and run the verification check.