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) 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.
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. 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 | The Pandectes Google Consent Mode integration |
Pixel sandbox | The | The code in this article |
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) |
No GTM, properties loaded with | 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.
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:
consentdefault: the store policy, written into the code.consentupdate: whateverinit.customerPrivacycontains. If it was only the default, nothing changes. If it was a saved choice, it applies before the first hit.- Load
gtag.jsand configure your Google properties. - Listen for
visitorConsentCollectedand send anupdateevery 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 | |
|---|---|
|
|
|
|
|
|
| No Consent Mode equivalent. It is a US state sale/share signal (see |
(always) |
|
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.
- In Shopify Admin, go to Settings > Customer events and click Add custom pixel.
- Give the pixel a name and paste one of the snippets.
- Fill in your IDs between
START_MODIFY_FOR_YOUR_STOREandEND_MODIFY_FOR_YOUR_STORE. - Add your own event subscriptions at the bottom.
- 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.
/* 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.
/* 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
- Open Chrome DevTools and go to the Console tab.
- In the console toolbar, open the frame dropdown (it normally shows
top) and select the pixel's sandbox frame (its name starts withweb-pixel). Shopify creates one frame per custom pixel, so pick the right one if you have several. - 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, theconsententries must come before theconfigentries. - Does consent reach the tag? Set
DEBUG = true, uncomment an event so something is sent, and open GA4 DebugView. Withanalytics_storagedenied 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.loginside thevisitorConsentCollectedcallback 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 |
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 | Every order is counted twice in Google Ads. |
|
|
Skipping the | Returning visitors with saved consent get denied tags on their first page view. |
| 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 | Neither looks inside the pixel sandbox. |
| Breaks fraud prevention signals with no compliance benefit. |
| 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 |
|---|---|
| Whether ad click IDs such as GCLID are passed in the URL between pages when cookies cannot be set |
| Whether ad click IDs are removed from cookieless pings while |
| Whether hashed customer data may be sent with conversions |
| The US state sale/share restriction, related to Shopify's |
| 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:
// 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.
Updated on: 30/10/2026
Thank you!
