August 4, 20264 min read

Sharing cookies between a headless Shopify storefront and its checkout

Tweek-Eek is a headless Shopify store. The storefront runs on www.tweek-eek.com, the checkout on checkout.tweek-eek.com.

Accept cookies on the storefront and the checkout asks again. The fix is one call: hand the visitor's choice to Shopify's Customer Privacy API and let it write the consent cookie on tweek-eek.com.

Two reasons.

The banner cookie is host-only. setCookie('cookie_consent', 'true', { path: '/' }) belongs to www.tweek-eek.com. The browser sees checkout.tweek-eek.com as a different host and does not send the cookie there. The Domain attribute widens a cookie to tweek-eek.com and every host under it.

The checkout does not read that cookie anyway. Shopify keeps consent in _tracking_consent, an opaque token it generates itself. That is the cookie that has to reach the checkout.

Hand the choice to Shopify

Load the Customer Privacy API. It puts window.Shopify.customerPrivacy on the page.

TypeScript
<script src="https://cdn.shopify.com/shopifycloud/consent-tracking-api/v0.1/consent-tracking-api.js"></script>

There is a second build, storefront-banner.js, that bundles Shopify's own banner UI. Take that one if you do not have a banner. We do, so the bare API is enough.

Then call setTrackingConsent with the choice. A headless storefront needs four extra fields: without them the API assumes it is running on the online store and has no idea where to put the cookie.

TypeScript
window.Shopify.customerPrivacy.setTrackingConsent(
  {
    marketing: granted,
    analytics: granted,
    preferences: granted,
    sale_of_data: granted,
    headlessStorefront: true,
    checkoutRootDomain: 'checkout.tweek-eek.com',
    storefrontRootDomain: 'tweek-eek.com',
    storefrontAccessToken: STOREFRONT_ACCESS_TOKEN,
  },
  (result) => {
    if (result?.error) console.error('Shopify consent error:', result.error)
  },
)

The token is the public Storefront API token, so it can sit in the browser. storefrontRootDomain is the domain the two hosts share. That is it: the checkout reads the cookie and skips its own banner.

Call it again with false when the visitor declines. A rejection has to travel too, otherwise the checkout keeps asking.

You can read the token out of the Storefront API and set _tracking_consent by hand. It works today. It is also the one thing the docs tell you not to do — "never read/modify any Shopify cookies directly" — because the format is theirs to change.

And they changed it. Shopify stopped setting _tracking_consent, _landing_page and _orig_referrer itself on 15 September 2025, and points at the Customer Privacy API for consent. The older headless flow is deprecated inside the script too: pass headlessStorefront: true without a storefrontAccessToken and it logs Headless consent has been updated.

The by-hand version has a trap of its own. cookies-next encodes the value again on write — the cookie package it uses defaults to encodeURIComponent — so a token that arrives URL-encoded is stored double-encoded and the checkout cannot read it. That is the kind of detail you inherit the moment you take the cookie into your own hands.

Those three together, per RFC 6265 §5.3. Write _tracking_consent with a different Domain and you do not update the old cookie, you add a second one. The browser sends both and the reader picks one of them.

This is the failure you are most likely to hit right after switching over: a host-only _tracking_consent left behind by an earlier hand-rolled banner, sitting next to the one Shopify now writes on the shared root. Delete it once.

TypeScript
document.cookie = '_tracking_consent=; Path=/; Max-Age=0'

What the API does under the hood

Read consent-tracking-api.js and it is not much. It posts the visitor's choices to the consentManagement query of the Storefront API on the checkout domain, and writes the token it gets back.

TypeScript
query {
  consentManagement {
    cookies(
      visitorConsent: { marketing: true, analytics: true, preferences: true }
      origReferrer: ""
      landingPage: "/"
    ) {
      trackingConsentCookie
      cookieDomain
      landingPageCookie
      origReferrerCookie
    }
    customerAccountUrl
  }
}

cookieDomain comes back from the API. The script writes _tracking_consent on that domain, and a second time on storefrontRootDomain when the two differ. Path /, one year, and — worth noting if you were about to copy it — no Secure and no SameSite, so Lax. Enough, because storefront and checkout are the same site.

The endpoint is the interesting part. consentManagement is not in the Storefront API reference and not in the versioned schema. The 2026-04 schema that ships with hydrogen-react does not have the field, and /api/2026-04/graphql.json answers Field 'consentManagement' doesn't exist on type 'QueryRoot'. Shopify posts to /api/unstable/graphql.json, and describes unstable as continuously updated, with features that can be added or removed at any time. Which is the whole argument for letting the script own that call instead of copying it into your own bundle.

Verifying

Open DevTools → Application → Cookies on the storefront. _tracking_consent should show .tweek-eek.com in the Domain column, not the storefront host. Click through to the checkout: same cookie, same value, no second banner.

Two rows with the same name and a different domain means you hit the problem above. Delete the host-only one.