> ## 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).

# How to verify that Pandectes works with the Shopify Customer Privacy API

### Overview

Pandectes GDPR Compliance is integrated with Shopify's Customer Privacy API. When a visitor chooses **Accept**, **Reject**, or a custom selection in the preferences window, the banner reports that choice to Shopify through the official API. Shopify then applies it to the surfaces it controls: web pixels, customer events, audiences, and checkout.

This article shows how to confirm that the signal arrives in your own store. It also lists checks that look like failures but are not.

For how the integration behaves, see [How the app works with Shopify's Customer Privacy API](/en/article/how-the-app-works-with-shopifys-customer-privacy-api-1wqe4u6/). Shopify's own reference is the [Customer Privacy API](https://shopify.dev/docs/api/customer-privacy).

|| This guide is written for a normal Shopify storefront (Online Store theme). On a headless or custom storefront the same checks apply, with a few practical limits. Read Headless and custom storefronts before you start.

### How it works

The app's job in this integration is to pass the visitor's decision to Shopify, correctly and immediately. Everything afterwards belongs to Shopify: loading or holding back web pixels on the storefront and in checkout, gating sales channels such as Google & YouTube or Meta, and applying consent to customer events, audiences, and checkout.

Those are Shopify-owned systems that read consent from Shopify's own API. Once the checks below show that the signal arrives, the app has done what it is meant to do.

`setTrackingConsent()` is the documented way for an app to tell Shopify what a visitor decided. Pandectes calls it. That call is the interface between the app and Shopify.

### Key points

* Use `currentVisitorConsent()` to verify the visitor's raw decision (`''`, `'yes'`, or `'no'`).
* The `Allowed()` methods mix merchant settings, location, and consent. A `true` or `false` alone does not prove that the visitor decided anything.
* `shouldShowBanner()` is a region check. It can stay `true` after Accept.
* Do not read Shopify cookies, `dataLayer`, or network requests to judge this integration.
* The first check installs nothing. The theme snippet and the test pixel are temporary. Remove them when you are done.

| In a hurry? Do the Quick Console Check only. If the values go from empty to `'yes'` when you click Accept, the integration is writing consent to Shopify.

### Before you start

Three things, or the results will be misleading:

1. **Use a clean browser profile.** Open a new Incognito / Private window, or clear cookies and local storage for your domain. If you already gave consent, the banner will not ask again and you will only see the "after" state.
2. **Test as a visitor from a region where consent is required** (for example the EU/EEA/UK if that is how your banner is configured). In regions where Shopify is not configured to require consent, everything is allowed by default and the test proves nothing.
3. **Make sure the Pandectes app embed is enabled** in your theme. The Shopify Customer Privacy integration is always active and has no setting to turn on.

Open the browser **Developer Console**:

* **Chrome / Edge:** press `F12`, then open the **Console** tab
* **Firefox:** press `F12`, then open the **Console** tab
* **Safari:** enable the *Develop* menu in Settings → Advanced, then `Option + Command + C`

||| The first check installs nothing. The two snippets that follow are temporary diagnostic tools. Remove them as soon as verification is finished. They are meant to run for a few minutes on your screen, not to stay on a live store.

### What Pandectes sends to Shopify

Shopify's API uses four purposes. Pandectes maps its cookie categories onto them:

| Pandectes category | Shopify purpose |
| --- | --- |
| Functionality / Preferences | `preferences` |
| Performance / Analytics | `analytics` |
| Targeting / Marketing | `marketing` |
| Sale of data opt-out (Do not sell my data) | `sale_of_data` |

Strictly necessary cookies have no equivalent. They are always allowed and are not part of this consent write.

### Option 1: Quick Console Check

You do not need to install anything. One command, run three times, is enough.

**Step 1.** Open your storefront in a new Incognito window. The banner appears. Do not click anything.

**Step 2.** Open the Console, paste this, and press Enter:

```javascript
window.Shopify.customerPrivacy.currentVisitorConsent();
```

You should get four empty strings. That is Shopify's way of saying the visitor has not decided yet:

```javascript
{marketing: '', analytics: '', preferences: '', sale_of_data: ''}
```

**Step 3.** Click **Accept all** on the banner, then run the same command again:

```javascript
{marketing: 'yes', analytics: 'yes', preferences: 'yes', sale_of_data: ''}
```

**Step 4.** Reload the page and run it one last time. The values must still be there.

Empty before the decision, filled in right after it, still filled in on the next page load. That is the verification. If you click **Reject all** instead, you get `'no'` in place of `'yes'`. That is equally correct: the visitor was asked and said no.

Only `preferences`, `analytics`, and `marketing` need to change. `sale_of_data` can stay empty. That is normal. See the note under What you should not use to verify.

|| If the command returns an error saying `customerPrivacy` is undefined, the API has not finished loading yet. Wait a moment and try again, or run this once first:

```javascript
window.Shopify.loadFeatures([{name: 'consent-tracking-api', version: '0.1'}], () => {});
```

#### Why currentVisitorConsent and not marketingAllowed

Merchants often check only this:

```javascript
window.Shopify.customerPrivacy.preferencesProcessingAllowed();  // true / false
```

That method is useful, but it can only answer `true` or `false`. Shopify's documentation explains that these methods combine merchant settings, user location, and user consent.

So `false` might mean the visitor refused, or that the visitor is in a consent-required region and has not answered yet. `true` might mean the visitor accepted, or that this region does not require consent. The boolean alone cannot tell you whether the visitor decided anything.

`currentVisitorConsent()` returns the preferences selected by the user and does not mix in location or merchant configuration:

```javascript
window.Shopify.customerPrivacy.currentVisitorConsent();
// → {marketing: 'yes', analytics: 'no', preferences: 'yes', sale_of_data: ''}
```

| Value | Meaning |
| --- | --- |
| `''` (empty string) | The visitor has not yet granted or denied consent |
| `'yes'` | The visitor actively granted consent |
| `'no'` | The visitor actively denied consent |

Empty before the visitor decides, filled in afterwards. That transition is the proof that Pandectes wrote the decision into Shopify.

### Option 2: On-Screen Theme Test

This snippet automates the Quick Console Check. It prints the consent state to the console when the page loads, and again when the visitor decides. It only reads and logs. It changes nothing.

#### Where to paste the theme snippet

1. Shopify admin → **Online Store → Themes**
2. On your live theme click **… → Edit code** *(safer: **Duplicate** the theme first and test on the copy)*
3. Open `layout/theme.liquid`
4. Paste the snippet **immediately before the closing `</body>` tag**
5. **Save**

```html
{% comment %} PANDECTES CONSENT CHECK: temporary, remove after testing {% endcomment %}
<script>
(function () {
  var pandectesTag = '[Pandectes theme check]';

  function pandectesSnapshot(label) {
    var cp = window.Shopify && window.Shopify.customerPrivacy;
    if (!cp) {
      console.warn(pandectesTag, label, 'API not available');
      return;
    }

    var consent = cp.currentVisitorConsent();

    console.log('%c' + pandectesTag + ' ' + label, 'font-weight:bold;color:#0a7');
    console.table({
      'currentVisitorConsent().preferences' : consent.preferences,
      'currentVisitorConsent().analytics'   : consent.analytics,
      'currentVisitorConsent().marketing'   : consent.marketing,
      'currentVisitorConsent().sale_of_data': consent.sale_of_data,
      'preferencesProcessingAllowed()'      : cp.preferencesProcessingAllowed(),
      'analyticsProcessingAllowed()'        : cp.analyticsProcessingAllowed(),
      'marketingAllowed()'                  : cp.marketingAllowed(),
      'saleOfDataAllowed()'                 : cp.saleOfDataAllowed()
    });
  }

  document.addEventListener('visitorConsentCollected', function (event) {
    console.log(pandectesTag, 'visitorConsentCollected →', event.detail);
    pandectesSnapshot('AFTER the visitor decided');
  });

  (function pandectesWaitForShopify(tries) {
    tries = tries || 0;
    if (window.Shopify && window.Shopify.loadFeatures) {
      window.Shopify.loadFeatures(
        [{ name: 'consent-tracking-api', version: '0.1' }],
        function (error) {
          if (error) {
            console.error(pandectesTag, 'could not load the Customer Privacy API', error);
            return;
          }
          pandectesSnapshot('ON PAGE LOAD');
        }
      );
      return;
    }
    if (tries > 100) {
      console.error(pandectesTag, 'window.Shopify.loadFeatures never became available');
      return;
    }
    setTimeout(function () { pandectesWaitForShopify(tries + 1); }, 100);
  })();
})();
</script>
```

#### How to run the theme test

Open your storefront in a **new Incognito window**, open the **Console**, and follow these three steps.

**Step 1: Before touching the banner**

The banner is on screen. You have not clicked anything. Look for the `ON PAGE LOAD` table.

| What you should see | Meaning |
| --- | --- |
| `currentVisitorConsent().preferences` → `""` *(empty)* | No decision made yet |
| `currentVisitorConsent().analytics` → `""` *(empty)* | No decision made yet |
| `currentVisitorConsent().marketing` → `""` *(empty)* | No decision made yet |
| `preferencesProcessingAllowed()` → `false` | Nothing non-essential is allowed |
| `analyticsProcessingAllowed()` → `false` | Nothing non-essential is allowed |
| `marketingAllowed()` → `false` | Nothing non-essential is allowed |

The empty strings are the important part. Shopify is saying this visitor has not decided yet.

**Step 2: Click a button on the banner**

Click **Accept all**. Two things must appear in the console immediately:

1. A line `visitorConsentCollected → {marketingAllowed: true, saleOfDataAllowed: …, analyticsAllowed: true, preferencesAllowed: true}`
2. A second table, `AFTER the visitor decided`:

| What you should see | Meaning |
| --- | --- |
| `currentVisitorConsent().preferences` → `"yes"` | Consent actively granted |
| `currentVisitorConsent().analytics` → `"yes"` | Consent actively granted |
| `currentVisitorConsent().marketing` → `"yes"` | Consent actively granted |
| `preferencesProcessingAllowed()` → `true` | Processing now permitted |
| `analyticsProcessingAllowed()` → `true` | Processing now permitted |
| `marketingAllowed()` → `true` | Processing now permitted |

If you click **Reject all** instead, the values become `"no"` and the `Allowed()` methods stay `false`. `"no"` is not the same as `""`: the visitor was asked and said no.

**Step 3: Reload the page**

Reload and look at the new `ON PAGE LOAD` table. The values from Step 2 must still be there. That proves Shopify stored the decision and returns it on later pages.

If Steps 1, 2, and 3 behave as above, the integration is writing consent to Shopify.

||| Now remove the snippet from `layout/theme.liquid` and save. Do not leave it on a live theme. It is a testing tool, not part of your store.

### Option 3: Shopify Pixel Test

The theme snippet proves Shopify recorded the decision. This second snippet proves the decision reaches Shopify's web pixels (Google & YouTube, Meta, Klaviyo, and any custom pixel).

Pixels run inside a sandbox, separate from your theme. Shopify hands them the consent state directly. The snippet creates a small test pixel that reports what it was handed.

#### Where to paste the pixel snippet

1. Shopify admin → **Settings → Customer events**
2. Click **Add custom pixel**, name it `Pandectes consent check`
3. Under **Customer privacy**, set **Permission** to **Not required**, so this test pixel always runs and can report the "before" state too
4. Paste the code, click **Save**, then **Connect**

```javascript
// Pandectes consent check. TEMPORARY test pixel. Disconnect and delete when finished.
const pandectesTag = '[Pandectes pixel check]';

function pandectesLog(label, cp) {
  console.log(pandectesTag + ' ' + label, {
    preferencesProcessingAllowed: cp.preferencesProcessingAllowed,
    analyticsProcessingAllowed:   cp.analyticsProcessingAllowed,
    marketingAllowed:             cp.marketingAllowed,
    saleOfDataAllowed:            cp.saleOfDataAllowed
  });
}

let pandectesConsentState = init.customerPrivacy;
pandectesLog('state when the pixel started', pandectesConsentState);

api.customerPrivacy.subscribe('visitorConsentCollected', (event) => {
  pandectesConsentState = event.customerPrivacy;
  pandectesLog('state after the visitor decided', pandectesConsentState);
});

analytics.subscribe('page_viewed', (event) => {
  pandectesLog('state at page_viewed', pandectesConsentState);
});
```

|| Why the long variable names? The pixel sandbox already defines globals such as `status` and `name`, so a plain `let status = …` is reported as a redefinition. Every name in both snippets is prefixed with `pandectes`. Keep them as they are.

#### What you should see in the pixel test

Again in a fresh Incognito window with the console open:

| Moment | Expected log |
| --- | --- |
| Page loads, banner visible, nothing clicked | `state when the pixel started` → `preferencesProcessingAllowed`, `analyticsProcessingAllowed`, and `marketingAllowed` all `false` |
| You click **Accept all** | `state after the visitor decided` → the same three are now `true` |
| You reload the page | `state when the pixel started` → the three are still `true` |

`saleOfDataAllowed` is not part of this check. Outside regions with a data-sale opt-out it can be `true` from the start. That is expected.

|| These lines come from Shopify's pixel sandbox, so in Chrome they appear with a `web-pixels…sandbox` source next to them. If you do not see them, make sure the console filter is not set to "selected context only" and that no filter text is typed in the filter box.

If those three values go from `false` to `true` when you click Accept, Shopify is handing the consent state to pixels.

||| Disconnect and delete this test pixel as soon as you are finished. It is set to run without consent, so it must not stay on a live store.

### Once the signal arrives, the rest is Shopify's job

Shopify's web pixels run inside sandboxed, cross-origin iframes that Shopify controls. No consent app can start them, stop them, or change what they send. The consent signal is the lever that exists.

The verification splits in two:

1. **Does the signal arrive?** That is what the checks above answer. That is the part Pandectes is responsible for.
2. **Does Shopify act on the signal?** That is a Shopify question, answered in Shopify's settings and, if needed, by Shopify support.

If `currentVisitorConsent()` goes from empty to `'yes'` or `'no'` when the visitor clicks, the app has done its job. If a pixel or a channel still behaves in a way you did not expect after that, look on Shopify's side:

| Who | Does what |
| --- | --- |
| **Pandectes** | Shows the banner, collects the decision, writes it to Shopify via `setTrackingConsent()` |
| **Shopify** | Decides which web pixels load on the storefront and in checkout, gates sales channels, and applies consent to customer events, audiences, and checkout |

* **Settings → Customer events:** each pixel has its own **Customer privacy → Permission** setting. A pixel set to *Not required* will run regardless of consent, because that is what the store told Shopify to do.
* **Each sales channel's own configuration.** The Google & YouTube channel, for example, builds its own Google consent signal from Shopify's API. That translation is Shopify's code.
* **Shopify support**, for anything that remains after the API holds the right value.

Think of it as a light switch: Pandectes is the switch, Shopify is the wiring and the lamps. Once you have verified the switch flips, a lamp that stays on is a wiring question.

### Headless and custom storefronts

The same API, the same methods, and the same meaning for `''`, `'yes'`, and `'no'` apply on a headless store. The Quick Console Check works unchanged. What changes is how you run the two snippets, plus one extra setting.

#### You have no theme.liquid

Your storefront pages are your own code, so there is nowhere to paste the theme snippet as written. Two options:

**Option A: paste it in the browser console (nothing to deploy).**
Copy everything between `<script>` and `</script>` in the theme snippet, open your storefront in a clean Incognito window with the banner showing, paste it into the Console, and press Enter. You get the `ON PAGE LOAD` table immediately, and the `AFTER the visitor decided` table when you click the banner.

|| Console code is wiped by a page reload. For the reload step, reload first, then paste the snippet again to read the stored values.

**Option B: put it in your own root layout.**
Add the same `<script>` block to whatever renders on every page: the root layout in Hydrogen or Next.js, `index.html` in a Vite/React app, and so on. Remove it once testing is done.

#### Snippet 2 runs on checkout, not on your storefront

Shopify's web pixels do not run on pages you host yourself. They run on surfaces Shopify owns: **checkout** and the **order status / thank-you page**. To see the pixel snippet output, add a product to the cart and continue to checkout with the console open. The consent decision made on your storefront should already be in place when the pixel starts.

#### One extra setting: your two domains

On a headless store, Shopify needs the storefront and checkout domains before it can record consent. Shopify's documentation requires these values for custom storefronts:

* `headlessStorefront`
* `storefrontRootDomain`
* `checkoutRootDomain`
* `storefrontAccessToken`

Pandectes sends them with every consent write, from your app configuration. If they are missing or wrong in the Pandectes admin, the write to Shopify cannot succeed, and `currentVisitorConsent()` will keep returning empty strings. If your headless test fails, check these first.

The domains also have to be a matching pair: storefront domain and checkout domain must belong to the same store, so the decision made on the storefront is the one checkout sees.

### What you should not use to verify

These checks either measure something else, or are not part of the documented API.

#### Don't expect shouldShowBanner to turn false after consent

Many merchants expect `shouldShowBanner()` to be `true` before consent and `false` afterwards, and report a bug when it stays `true`.

It is not a consent method. Shopify documents it as indicating whether the current customer is in a region that is configured to show a cookie banner. A German visitor is still in Germany after Accept, so the answer is still `true`. A `shouldShowBanner()` that stays `true` after consent is expected.

The same applies to `saleOfDataRegion()`, which indicates whether the current customer is in a region configured for data-sale opt-outs. That is also a region check.

#### Don't rely on getTrackingConsent, getRegulation, or isRegulationEnforced

These do not appear in Shopify's Customer Privacy API documentation. They are internal values that Shopify can change or remove. In particular, `getTrackingConsent()` returning `"yes"` before consent is not evidence that anything was granted.

#### Don't use userCanBeTracked or userDataCanBeSold

These exist, but Shopify lists them under legacy documentation and says not to begin new integrations with them. Use `currentVisitorConsent()` and the four `Allowed()` methods instead.

#### Don't look for a _tracking_consent cookie in document.cookie

Shopify's documentation says not to read or modify Shopify cookies directly. Shopify does not promise that its consent cookie is readable from JavaScript, that it has a particular name, or that it exists in a given release. Not finding it proves nothing.

#### Don't judge the app by whether a pixel or a network request still fires

A request leaving the browser is a Shopify outcome. If the consent signal is correct and a Shopify pixel still fires, Shopify was told the right thing and chose to run the pixel, usually because that pixel's **Permission** is *Not required* in **Settings → Customer events**, or because the sales channel is configured to run without consent. Fix it there.

#### Don't compare entries in window.dataLayer

The `dataLayer` is Google Consent Mode, a separate system from the Shopify Customer Privacy API. Pandectes writes to both, and Shopify's Google & YouTube channel also writes its own entries. Several `consent` entries there are normal and do not verify the Shopify API.

#### Don't expect a setTrackingConsent call on every page load

Pandectes only writes to Shopify when the state changes. If Shopify already holds the same decision, the app does not push it again. On a reload after consent, you should see the stored values but no new write.

#### Don't worry about sale_of_data staying empty

`sale_of_data` is relevant where a data-sale opt-out applies (mainly certain US states). If the visitor is not in such a region, or *Do not sell my data* is not enabled in Pandectes, this field is left out of what Pandectes sends. An empty `sale_of_data` for an EU visitor is expected.

### Quick reference: the documented methods

| Method | Returns | What it tells you |
| --- | --- | --- |
| `currentVisitorConsent()` | `{preferences, analytics, marketing, sale_of_data}` each `'yes'` / `'no'` / `''` | The visitor's own decision. Use this one. |
| `preferencesProcessingAllowed()` | `true` / `false` | Whether preference processing is allowed (region + settings + consent combined) |
| `analyticsProcessingAllowed()` | `true` / `false` | Whether analytics processing is allowed (region + settings + consent combined) |
| `marketingAllowed()` | `true` / `false` | Whether marketing is allowed (region + settings + consent combined) |
| `saleOfDataAllowed()` | `true` / `false` | Whether sharing data with third parties is allowed |
| `shouldShowBanner()` | `true` / `false` | Region only: whether a banner should be shown in this region |
| `saleOfDataRegion()` | `true` / `false` | Region only: whether a data-sale opt-out applies here |
| `getRegion()` | e.g. `"DEHH"` | The visitor's location, ISO 3166-2 format |
| `visitorConsentCollected` event | `{marketingAllowed, saleOfDataAllowed, analyticsAllowed, preferencesAllowed}` | Fires each time consent changes |

### If the test really does fail

First decide which half failed.

**If the signal is correct**, meaning `currentVisitorConsent()` moves from empty to `'yes'` or `'no'` when you click, then the app is writing consent. Anything else belongs to Shopify. Check each pixel's **Permission** in **Settings → Customer events**, then sales channel configuration, then Shopify support.

**If the signal is missing**, meaning `currentVisitorConsent()` still returns empty strings after you clicked a banner button, check in this order:

1. The Pandectes app embed is enabled in the theme you are testing (Online Store → Themes → Customize → App embeds).
2. You are testing the live theme, not a preview of a different one.
3. No other consent app is installed. Two consent apps writing to the same API will conflict. Only one may be active.
4. You are not in an admin preview session, which can behave differently from a real visit.
5. Headless stores only: the storefront domain, checkout domain, and storefront access token are filled in correctly in the Pandectes admin.

Still failing? Contact **support@pandectes.io** and include:

* your store domain (and, for headless, your storefront and checkout domains)
* the visitor country you tested from
* the console output of the Quick Console Check or of the theme snippet, at all three moments (before clicking / after clicking / after reload)

### Clean up when you're done

Do not skip this step. The Quick Console Check installs nothing. Both snippets are temporary.

1. **Theme snippet:** open `layout/theme.liquid`, delete the block that starts with `{% comment %} PANDECTES CONSENT CHECK`, and **Save**. If you tested on a duplicated theme, you can delete that theme copy.
2. **Pixel snippet:** go to **Settings → Customer events**, open `Pandectes consent check`, **Disconnect** it, then **Delete** it.
3. **Headless stores:** if you added the script to your own layout, remove it and redeploy. If you used the browser console, closing the tab is enough.

Why it matters:

* The theme snippet prints consent details to the console of every visitor, on every page, until you remove it.
* The test pixel is configured with **Permission: Not required**, so it runs before consent by design. That is correct for a short test and wrong for a live store.
* Both add work to every page load for no benefit once verification is done.

Removing them does not change the integration. The app keeps reporting consent to Shopify as before.

### Summary

Verify this integration with `currentVisitorConsent()`. Empty strings before a choice, `'yes'` or `'no'` immediately after, and the same values after reload, mean Pandectes wrote the decision to Shopify. Pixel loading, sales channels, and checkout behavior after that point are Shopify's responsibility. Remove any temporary theme or pixel snippets when you are finished.