> ## Knowledge Base Index
> Fetch the complete knowledge base index at: https://help.pandectes.io/sitemap.xml
> Use this file to discover available pages before exploring further.
> Pure-Markdown content can be obtained by appending a '.md' suffix to the content URLs listed in the sitemap (without the trailing slash).

# Google Consent Mode in a Shopify custom pixel (gtag.js)

### Overview

Some stores load Google Analytics 4 (`G-`) or Google Ads (`AW-`) directly with `gtag.js` from a **Shopify custom pixel** (**Settings > Customer events**). This article gives you ready-to-paste pixel code that applies Google Consent Mode Advanced to those properties, using the consent your visitors give on the Pandectes banner.

If your Google properties are loaded by a Google Tag Manager container, use [Google Consent Mode in a Shopify custom pixel (Google Tag Manager)](/en/article/google-consent-mode-in-a-shopify-custom-pixel-google-tag-manager-i46xi/) instead.

This is an implementation guide. For background on `default` and `update` commands and on Advanced mode, see Google's [Consent Mode with gtag.js](https://developers.google.com/tag-platform/security/guides/consent?consentmode=advanced#gtag.js_2).

### Before you start

#### Turn on the Google Consent Mode integration first

This pixel is one half of a two-part setup. In Shopify Admin, go to **Apps > Pandectes GDPR > Integrations**, click **Configure** on the **Google Consent Mode** card, and turn the integration on. See [Using Google Consent Mode Integration](/en/article/using-google-consent-mode-integration-1xkfv6o/). Google Consent Mode works on the **Plus** plan and above.

A Shopify custom pixel runs in a sandboxed iframe with its own `window`, `dataLayer` and copy of `gtag.js`. It is separate from the storefront page, and consent set in one is invisible to the other. So each needs its own setup:

| Where | What runs there | What sets consent mode |
| --- | --- | --- |
| Storefront page | Google tags in `theme.liquid`, your GTM container, tags from other apps | The Pandectes **Google Consent Mode** integration |
| Pixel sandbox | The `gtag.js` you load in the pixel and its properties | The code in this article |

||| The pixel does not replace the integration. Nothing in the pixel can reach the storefront page, so without the integration your storefront has no consent mode at all.

#### Do you use Google Tag Manager?

If your store has a GTM container, check whether your Google properties are already loaded there. They usually are. Adding a pixel that loads the same GA4 or Google Ads property again duplicates your page views, sessions, purchases and conversions, and creates two competing consent states.

| Your setup | Follow |
| --- | --- |
| GA4 or Google Ads loaded by a GTM container | [Google Consent Mode in a Shopify custom pixel (Google Tag Manager)](/en/article/google-consent-mode-in-a-shopify-custom-pixel-google-tag-manager-i46xi/) |
| No GTM, properties loaded with `gtag.js` in a custom pixel | This article |

### What this code covers

The code sets **the consent state and nothing else**. It sends Google the store's default policy, applies the visitor's choice, keeps it in sync with the banner, and loads the Google tag so the consent signals have somewhere to go. It does not send analytics or conversion data by itself.

||| **Pandectes provides the consent layer only.** Which events you send, what data they carry, how they map to your GA4 property or Google Ads conversions, and whether your reporting is accurate are your responsibility. Pandectes does not support or verify the tracking built on top of this code.

#### Your event subscriptions go in the same pixel

Sending Shopify's customer events to Google is the real purpose of the pixel, and those subscriptions must be in the **same pixel**, below the consent code. The examples in each snippet are commented out on purpose. Uncomment them or replace them with your own.

#### Every pixel that loads a Google property needs its own copy

You cannot put the consent code in one custom pixel and your events in another. Each pixel is a separate sandbox. A pixel that loads GA4 or Google Ads without the consent code fires everything under Google's defaults, as if no consent mode existed, and Shopify does not warn you.

* One pixel loads your properties: one copy of the code, with that pixel's events below it.
* Three pixels each load a Google property: three copies, one in each pixel.
* If several pixels load the **same** property, fix that duplication first.

### Advanced mode and the pixel permission

This code gives you **Google Consent Mode Advanced**: the Google tag loads on every page view, sends cookieless pings while consent is denied, and switches to full measurement once consent is granted. That is what lets Google model conversions for visitors who do not consent.

For that, the pixel must be allowed to run before consent. In Shopify Admin, go to **Settings > Customer events**, open your custom pixel, and under **Customer privacy** set **Permission** to **Not required**. If you set it to **Required**, Shopify holds the pixel back until the visitor accepts, so the Google tag never loads before consent and no cookieless pings are sent.

If you want **Basic mode** (nothing loads until consent is given), you do not need this code. Set **Permission** to **Required**, select the categories you need, and let Shopify hold the pixel back.

### How the code works

Shopify gives the pixel the visitor's consent in `init.customerPrivacy`, but it does not say whether that is the store default or a choice the visitor already made. So the code always runs these steps, in this order:

1. `consent` `default`: the store policy, written into the code.
2. `consent` `update`: whatever `init.customerPrivacy` contains. If it was only the default, nothing changes. If it was a saved choice, it applies before the first hit.
3. Load `gtag.js` and configure your Google properties.
4. Listen for `visitorConsentCollected` and send an `update` every time the visitor changes their choice.

In a custom pixel, `analytics`, `browser` and `init` are available directly, but `customerPrivacy` is reached through `api`. Calling `customerPrivacy.subscribe(...)` without `api.` throws a `ReferenceError`.

Shopify's consent values map to Google's consent types like this:

| Shopify | Google |
| --- | --- |
| `analyticsProcessingAllowed` | `analytics_storage` |
| `marketingAllowed` | `ad_storage`, `ad_user_data`, `ad_personalization` |
| `preferencesProcessingAllowed` | `functionality_storage`, `personalization_storage` |
| `saleOfDataAllowed` | No Consent Mode equivalent. It is a US state sale/share signal (see `restricted_data_processing`). |
| (always) | `security_storage` is always `granted` |

### Which snippet do I need?

| Your store's policy | Use |
| --- | --- |
| Same policy worldwide: nothing runs until the visitor consents | **Snippet 1** (one default command) |
| Consent collected only in some regions, allowed elsewhere | **Snippet 2** (default commands per region) |

Both snippets support GA4, Google Ads, or both.

1. In Shopify Admin, go to **Settings > Customer events** and click **Add custom pixel**.
2. Give the pixel a name and paste one of the snippets.
3. Fill in your IDs between `START_MODIFY_FOR_YOUR_STORE` and `END_MODIFY_FOR_YOUR_STORE`.
4. Add your own event subscriptions at the bottom.
5. Set the permission as described above, click **Save**, then **Connect**.

### Snippet 1: one default command

One `default` command with no region, so it applies to every visitor. Everything is denied except `security_storage`.

```javascript
/* START_MODIFY_FOR_YOUR_STORE */
const GA4_ID = ""; // e.g. "G-XXXXXXXXXX", leave empty if not used
const AW_ID = ""; // e.g. "AW-XXXXXXXXXX", leave empty if not used
const DEBUG = false; // true sends GA4 hits to DebugView while testing
/* END_MODIFY_FOR_YOUR_STORE */

const GRANTED = "granted";
const DENIED = "denied";
const GOOGLE_URL = "https://www.googletagmanager.com/gtag/js?id=";
const DEVELOPER_ID = "dMTZkMj";

/* Shopify customerPrivacy -> Google consent state */
const toConsentState = (cp) => ({
  ad_storage: cp.marketingAllowed ? GRANTED : DENIED,
  ad_user_data: cp.marketingAllowed ? GRANTED : DENIED,
  ad_personalization: cp.marketingAllowed ? GRANTED : DENIED,
  analytics_storage: cp.analyticsProcessingAllowed ? GRANTED : DENIED,
  functionality_storage: cp.preferencesProcessingAllowed ? GRANTED : DENIED,
  personalization_storage: cp.preferencesProcessingAllowed ? GRANTED : DENIED,
  security_storage: GRANTED,
});

/* The pixel sandbox has its own dataLayer. */
window.dataLayer = window.dataLayer || [];
function gtag() {
  dataLayer.push(arguments);
}

/* Pandectes developer ID. Identifies the implementation to Google,
 * has no effect on your data. Leave it as it is. */
gtag("set", `developer_id.${DEVELOPER_ID}`, true);

/* STEP 1: store policy, before gtag.js loads. Applies to everyone. */
gtag("consent", "default", {
  ad_storage: DENIED,
  ad_user_data: DENIED,
  ad_personalization: DENIED,
  analytics_storage: DENIED,
  functionality_storage: DENIED,
  personalization_storage: DENIED,
  security_storage: GRANTED,
});

/* STEP 2: visitor state, always sent. If it is only the default,
 * nothing changes. If it is a saved choice, it lands before the first hit. */
gtag("consent", "update", toConsentState(init.customerPrivacy));

/* STEP 3: load the Google tag once and configure each property. */
const TAG_IDS = [GA4_ID, AW_ID].filter(Boolean);

if (TAG_IDS.length) {
  gtag("js", new Date());

  if (GA4_ID) {
    gtag("config", GA4_ID, {
      send_page_view: false,
      ...(DEBUG ? { debug_mode: true } : {}),
    });
  }

  if (AW_ID) {
    gtag("config", AW_ID);
  }

  const script = document.createElement("script");
  script.async = true;
  script.src = GOOGLE_URL + TAG_IDS[0];
  document.head.appendChild(script);
}

/* STEP 4: follow the visitor's choices. Note the api. prefix. */
api.customerPrivacy.subscribe("visitorConsentCollected", (event) => {
  gtag("consent", "update", toConsentState(event.customerPrivacy));
});

/* YOUR EVENT SUBSCRIPTIONS GO HERE, IN THIS SAME PIXEL.
 * The examples are commented out on purpose. What you send is yours
 * to own and verify. */

/* Page view: only if the storefront does not already send page_view
 * for the same property, or it is counted twice. */
// analytics.subscribe("page_viewed", (event) => {
//   gtag("event", "page_view", {
//     page_path: event.context.document.location.pathname,
//     page_title: event.context.document.title,
//     page_location: event.context.window.location.href,
//   });
// });

/* Purchase: if GA4 and Google Ads are linked, import the GA4 purchase
 * as a conversion in Google Ads and delete the conversion block below,
 * or every order is counted twice. */
// analytics.subscribe("checkout_completed", (event) => {
//   const checkout = event.data.checkout;
//   const items = checkout.lineItems.map((line) => ({
//     item_id: "shopify_" + line.variant.product.id + "_" + line.variant.id,
//     item_name: line.title,
//     item_variant: line.variant.title,
//     price: line.variant.price.amount,
//     quantity: line.quantity,
//   }));
//
//   gtag("event", "purchase", {
//     transaction_id: checkout.token,
//     currency: checkout.currencyCode,
//     value: checkout.totalPrice.amount,
//     tax: checkout.totalTax.amount,
//     shipping: checkout.shippingLine ? checkout.shippingLine.price.amount : 0,
//     items: items,
//   });
//
//   // Google Ads conversion, ONLY when GA4 and Google Ads are NOT linked.
//   // Use the conversion label from your Google Ads conversion action.
//   gtag("event", "conversion", {
//     send_to: "AW-XXXXXXXXXX/AbC-D_efGhIjKlMnOp",
//     transaction_id: checkout.token,
//     currency: checkout.currencyCode,
//     value: checkout.totalPrice.amount,
//   });
// });
```

### Snippet 2: default commands per region

For stores that collect consent only where it is legally required. There are two `default` commands: a global one that grants everything, and one for the listed regions that denies everything except `security_storage`. When both match a visitor, the more specific region wins.

The rest is identical to Snippet 1.

```javascript
/* START_MODIFY_FOR_YOUR_STORE */
const GA4_ID = ""; // e.g. "G-XXXXXXXXXX", leave empty if not used
const AW_ID = ""; // e.g. "AW-XXXXXXXXXX", leave empty if not used

/* ISO 3166-2 codes where the store collects consent: EEA + UK below.
 * Add US states ("US-CA", "US-CO", ...) if you treat them as opt-in.
 * This list MUST match the regions where your banner collects consent. */
const RESTRICTED_REGIONS = [
  "AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR",
  "DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL",
  "PL", "PT", "RO", "SK", "SI", "ES", "SE", "GB", "LI", "NO", "IS",
];

const DEBUG = false; // true sends GA4 hits to DebugView while testing
/* END_MODIFY_FOR_YOUR_STORE */

const GRANTED = "granted";
const DENIED = "denied";
const GOOGLE_URL = "https://www.googletagmanager.com/gtag/js?id=";
const DEVELOPER_ID = "dMTZkMj";

/* Shopify customerPrivacy -> Google consent state */
const toConsentState = (cp) => ({
  ad_storage: cp.marketingAllowed ? GRANTED : DENIED,
  ad_user_data: cp.marketingAllowed ? GRANTED : DENIED,
  ad_personalization: cp.marketingAllowed ? GRANTED : DENIED,
  analytics_storage: cp.analyticsProcessingAllowed ? GRANTED : DENIED,
  functionality_storage: cp.preferencesProcessingAllowed ? GRANTED : DENIED,
  personalization_storage: cp.preferencesProcessingAllowed ? GRANTED : DENIED,
  security_storage: GRANTED,
});

/* The pixel sandbox has its own dataLayer. */
window.dataLayer = window.dataLayer || [];
function gtag() {
  dataLayer.push(arguments);
}

/* Pandectes developer ID. Identifies the implementation to Google,
 * has no effect on your data. Leave it as it is. */
gtag("set", `developer_id.${DEVELOPER_ID}`, true);

/* STEP 1a: policy for everyone outside the listed regions. */
gtag("consent", "default", {
  ad_storage: GRANTED,
  ad_user_data: GRANTED,
  ad_personalization: GRANTED,
  analytics_storage: GRANTED,
  functionality_storage: GRANTED,
  personalization_storage: GRANTED,
  security_storage: GRANTED,
});

/* STEP 1b: policy for the listed regions. Google resolves the region
 * from the IP address, so test it with a VPN. */
gtag("consent", "default", {
  ad_storage: DENIED,
  ad_user_data: DENIED,
  ad_personalization: DENIED,
  analytics_storage: DENIED,
  functionality_storage: DENIED,
  personalization_storage: DENIED,
  security_storage: GRANTED,
  region: RESTRICTED_REGIONS,
});

/* STEP 2: visitor state, always sent. Shopify has already applied your
 * regional settings to init.customerPrivacy, so this matches 1a or 1b. */
gtag("consent", "update", toConsentState(init.customerPrivacy));

/* STEP 3: load the Google tag once and configure each property. */
const TAG_IDS = [GA4_ID, AW_ID].filter(Boolean);

if (TAG_IDS.length) {
  gtag("js", new Date());

  if (GA4_ID) {
    gtag("config", GA4_ID, {
      send_page_view: false,
      ...(DEBUG ? { debug_mode: true } : {}),
    });
  }

  if (AW_ID) {
    gtag("config", AW_ID);
  }

  const script = document.createElement("script");
  script.async = true;
  script.src = GOOGLE_URL + TAG_IDS[0];
  document.head.appendChild(script);
}

/* STEP 4: follow the visitor's choices. Note the api. prefix. */
api.customerPrivacy.subscribe("visitorConsentCollected", (event) => {
  gtag("consent", "update", toConsentState(event.customerPrivacy));
});

/* YOUR EVENT SUBSCRIPTIONS GO HERE, IN THIS SAME PIXEL.
 * Use the same page view and purchase examples as in Snippet 1. */
```

### Check that it works

#### Tag Assistant cannot see the pixel

Google Tag Assistant inspects the storefront page only. The pixel's `gtag`, `dataLayer` and properties live in a sandboxed iframe that it does not reach, so it reports nothing (or only the storefront's own tags) even when the pixel works correctly. This is expected and is not an error.

#### Check the dataLayer in the pixel's frame

1. Open Chrome DevTools and go to the **Console** tab.
2. In the console toolbar, open the frame dropdown (it normally shows `top`) and select the pixel's sandbox frame (its name starts with `web-pixel`). Shopify creates one frame per custom pixel, so pick the right one if you have several.
3. Run the checks below.

In the `top` frame, `dataLayer` is the storefront's data layer, which tells you nothing about the pixel.

* **Do the defaults come first?** In `dataLayer`, the `consent` entries must come before the `config` entries.
* **Does consent reach the tag?** Set `DEBUG = true`, uncomment an event so something is sent, and open GA4 **DebugView**. With `analytics_storage` denied you see a cookieless ping instead of a normal hit.
* **Does the pixel load before consent?** It must, for Advanced mode. If the Google tag only appears after the visitor accepts, the pixel permission is set to **Required** and you are in Basic mode.
* **Does the region logic work?** Test with a VPN inside and outside a listed country.
* **Does the subscription fire?** Add a temporary `console.log` inside the `visitorConsentCollected` callback and change your choice on the banner.

### Common mistakes

| Mistake | What happens |
| --- | --- |
| Consent code in one pixel, events in another | The events pixel has no `gtag` and no consent state. Its events go nowhere. |
| Several pixels load a Google property, only one has the consent code | The others fire with no consent mode, with no warning. |
| Pixel **Permission** set to **Required** | The tag never loads before consent: no cookieless pings and no modelling. That is Basic mode. |
| Ads `conversion` event sent while the GA4 purchase is also imported as a conversion | Every order is counted twice in Google Ads. |
| `customerPrivacy.subscribe(...)` without `api.` | `ReferenceError`. Consent never updates after the banner. |
| Skipping the `update` because `init.customerPrivacy` looks like the default | Returning visitors with saved consent get denied tags on their first page view. |
| `default` sent after `config` | Consent Mode is not applied to the first hits. |
| Google Consent Mode integration not turned on in Pandectes | Only the pixel's properties get consent mode. Google tags on the storefront run without it. |
| Pixel built while GTM already loads the same property | Duplicate page views, sessions, purchases and conversions, plus two consent states. |
| Testing with Tag Assistant or reading `dataLayer` in the `top` frame | Neither looks inside the pixel sandbox. |
| `security_storage` denied | Breaks fraud prevention signals with no compliance benefit. |
| `RESTRICTED_REGIONS` does not match the banner's regions | The default and the update disagree. The update wins and the region policy is ignored. |

### Settings this article does not cover

The snippets leave out Google's account-level settings, so Google's defaults apply:

| Setting | What it controls |
| --- | --- |
| `url_passthrough` | Whether ad click IDs such as GCLID are passed in the URL between pages when cookies cannot be set |
| `ads_data_redaction` | Whether ad click IDs are removed from cookieless pings while `ad_storage` is denied |
| `allow_enhanced_conversions` | Whether hashed customer data may be sent with conversions |
| `restricted_data_processing` | The US state sale/share restriction, related to Shopify's `saleOfDataAllowed` |
| `wait_for_update` | How long tags wait for a consent update before firing |

Pandectes cannot advise on these. The right values depend on your Google accounts, the modelling you rely on, and your business's legal position. Discuss them with whoever manages your Google setup, or with your legal advisor where there is a privacy question.

If you decide to set one, add it as a `gtag("set", ...)` call **before** the `consent` `default` commands:

```javascript
// gtag("set", "url_passthrough", true);
// gtag("set", "ads_data_redaction", true);
```

`wait_for_update` is the exception: it goes inside a `default` command. These snippets do not need it, because `init.customerPrivacy` is available immediately and the `update` is sent straight after the defaults.