# Shopify

The c15t Shopify bridge sends each consent choice from Hydrogen to the Shopify Customer Privacy API. Shopify analytics then follow the c15t banner.

## Key facts

- **Tool:** Shopify
- **Category:** Commerce and CMS
- **Consent categories:** Measurement, Marketing, Functionality
- **Setup method:** c15t package
- **Package:** @c15t/integrations

## Links

- [Documentation](https://c15t.com/docs/integrations/shopify)
- [Shopify](https://www.shopify.com/)

Use your c15t banner as the custom consent banner of your Hydrogen storefront. Shopify analytics, plus the apps and pixels that read the Customer Privacy API, then follow each choice.

Inth keeps the consent record of each choice, with the visitor’s country and the policy that applied.

[Start for free](/dashboard/sign-up)

## Before you start

You need an Inth project that connects to your app, a Next.js storefront on Shopify Hydrogen and c15t in that app.

The bridge comes from @c15t/integrations/shopify. Hydrogen ShopifyScripts loads the Customer Privacy API.

[Open the Next.js quickstart](https://c15t.com/docs/frameworks/next/quickstart)

## How consent works with Shopify

The bridge maps c15t categories to Shopify values. Analytics follows measurement, marketing and sale\_of\_data follow marketing, and preferences follows functionality.

Each Shopify value is true only while its c15t category has permission. Policy restrictions count. Pass categories to change which category feeds each value.

Under an opt-in policy, setup waits for an answer from the visitor, Reject All too. So Hydrogen holds its events until then.

After the first choice, the bridge sends each change once. On withdrawal, c15t reloads the page, and the bridge sends the stored choice on load.

## Installation

Shopify has one consent mode with c15t. Your c15t banner is the custom banner, and the bridge passes each choice to Shopify.

### Wait for permission

The bridge sends nothing to Shopify until the visitor makes a choice. Then it sends each later change.

```ts
consent={{ mode: 'custom-banner', setup: shopifyPrivacy.setup }}
```

Under an opt-out policy, setup resolves without a choice. Shopify then applies its region defaults, Global Privacy Control too, until the visitor saves a choice in c15t.

## Setup steps

### Next.js

#### Install the integration package

Set up c15t with the Next.js quickstart first. Then add the integrations package. The Shopify bridge loads no script.

```
pnpm add @c15t/integrations
```

#### Create the bridge

Create the bridge once per page and pass its setup to Hydrogen ShopifyScripts with mode custom-banner. A second component feeds the bridge each c15t snapshot.

```
'use client';

import { createShopifyCustomerPrivacy } from '@c15t/integrations/shopify';
import type { ShopifyScriptsI18n, ShopifyScriptsShop } from '@shopify/hydrogen';
import { ShopifyScripts } from '@shopify/hydrogen/react';
import { useSnapshot } from 'c15t/next';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';

// One bridge per page. It sends each c15t choice to Shopify's Customer
// Privacy API, and its `setup` holds Hydrogen's analytics until Shopify has
// the visitor's choice.
const shopifyPrivacy = createShopifyCustomerPrivacy();

/** Hydrogen's scripts and analytics bus. Render it in the document head. */
export const ShopifyAnalytics = ({
  i18n,
  shop,
}: {
  i18n: ShopifyScriptsI18n;
  shop: ShopifyScriptsShop;
}) => {
  const router = useRouter();
  return (
    <ShopifyScripts
      analytics={{ channel: 'headless' }}
      consent={{ mode: 'custom-banner', setup: shopifyPrivacy.setup }}
      i18n={i18n}
      // Shopify's scripts route storefront links through Next's router.
      navigate={(url) => router.push(url)}
      shop={shop}
    />
  );
};

/** Feeds each c15t snapshot to the bridge. Render it inside `ConsentRoot`. */
export const ShopifyConsentSync = () => {
  const snapshot = useSnapshot();
  useEffect(() => shopifyPrivacy.update(snapshot), [snapshot]);
  return null;
};
```

#### Render both components in your layout

Render ShopifyAnalytics in the document head, as Hydrogen needs, with the details of your shop. Render ShopifyConsentSync inside ConsentRoot.

```
import {
  ConsentBanner,
  ConsentDialog,
  ConsentDialogLink,
  ConsentRoot,
} from 'c15t/next';
import { resolveConsent } from 'c15t/next/server';
import type { ReactNode } from 'react';

import {
  ShopifyAnalytics,
  ShopifyConsentSync,
} from '@/components/shopify-analytics';
import { currency, i18n, shop } from '@/lib/shop';

const RootLayout = async ({ children }: { children: ReactNode }) => {
  return (
    <html lang="en">
      <head>
        {/* Hydrogen's scripts belong in the head, once per page. */}
        <ShopifyAnalytics i18n={{ ...i18n, currency }} shop={shop} />
      </head>
      <body>
        <ConsentRoot state={resolveConsent()}>
          <ShopifyConsentSync />
          {children}
          <ConsentBanner />
          <ConsentDialog />
          <footer className="site-footer">
            <ConsentDialogLink>Privacy settings</ConsentDialogLink>
          </footer>
        </ConsentRoot>
      </body>
    </html>
  );
};

export default RootLayout;
```

This layout is a shorter cut of the c15t Shopify example. Keep your own providers, header and the rest of your layout.

#### Declare the categories

Shopify scripts load through ShopifyScripts, not through the c15t scripts option, so c15t cannot find them. Declare the categories that the bridge maps, or the dialog shows only Necessary and Shopify gets a denial for each value.

```
import { defineConsentConfig } from 'c15t/next';

export default defineConsentConfig({
  options: {
    // The categories the Shopify bridge maps. Shopify's scripts load through
    // Hydrogen's `ShopifyScripts`, not `scripts`, so c15t can't discover them.
    consentCategories: ['measurement', 'marketing', 'functionality'],
  },
});
```

These categories must also be in the scope of your policy.

#### Serve the Storefront API route

The Customer Privacy API saves each choice with a post to /api/\<version>/graphql.json on your storefront. Hydrogen handleShopifyRoutes serves that route, forwards it to your shop and keeps the cookies that Shopify sets. In Next.js, call it from proxy.ts.

```
import {
  createShopifyRequestContext,
  createStorefrontClient,
  handleShopifyRoutes,
} from '@shopify/hydrogen';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

import { cartHandlers } from '@/lib/cart-handlers';
import { i18n, storeDomain } from '@/lib/shop';

// This store has no customer accounts, so nothing keeps a session.
const noSession = {
  getSessionItem: () => undefined,
  getSessionOrigin: () => '',
  removeSessionItem: () => undefined,
  setSessionItem: () => undefined,
};

/**
 * Serves Hydrogen's routes: `/api/cart` for the cart, and the Storefront API
 * at `/api/<version>/graphql.json`, where Shopify's Customer Privacy API
 * saves each consent choice.
 */
export const proxy = (request: NextRequest) => {
  const requestContext = createShopifyRequestContext({ i18n, request });
  const storefrontClient = createStorefrontClient({
    config: { publicStorefrontToken: undefined, storeDomain },
    requestContext,
    type: 'public',
  });

  const shopifyRoute = handleShopifyRoutes({
    handlers: [cartHandlers],
    request,
    requestContext,
    sessionManager: noSession,
    storefrontClient,
  });
  if (shopifyRoute) {
    return shopifyRoute;
  }

  const response = NextResponse.next({
    request: { headers: requestContext.getForwardedRequestHeaders() },
  });
  requestContext.applyResponseHeaders(response.headers);
  return response;
};

export const config = {
  matcher: ['/((?!_next/static|_next/image|_next/data|favicon.ico).*)'],
};
```

The consent route needs no handlers. The example adds the Hydrogen cart handlers for its cart. If customers sign in to your storefront, pass your own session manager.

Checkout runs on the Shopify domain, and Shopify reads the stored choice there. The bridge maps categories only. It does not translate IAB TCF choices.

## Verify your setup

Use a private window with an opt-in policy. Filter DevTools Network by graphql.json. Then test the three states.

- **Before a choice**: No graphql.json request with visitorConsent goes out.

- **After Reject All**: One visitorConsent request goes out, and each of the four values is false.

- **After you allow Marketing**: Open the dialog and save. One request goes out with marketing and saleOfData set to true.

On a real shop, run window.Shopify.customerPrivacy.currentVisitorConsent() in the console and match it to your choice. mock.shop sets no consent cookie.

[Open the full Shopify guide](https://c15t.com/docs/integrations/shopify)

## Related guides

- [Adobe Analytics](/integrations/adobe-analytics)

- [Ahrefs Analytics](/integrations/ahrefs-analytics)

- [Databuddy](/integrations/databuddy)