# FAQ (/docs/consent-management/testing-and-troubleshooting/faq)

## How do I display the built-in consent manager UI? [#how-do-i-display-the-built-in-consent-manager-ui]

```javascript
await transcend.showConsentManager({ viewState: 'CompleteOptions' });
```

The available values for `viewState` can be found in our open source [consent manager UI repository](https://github.com/transcend-io/consent-manager-ui#view-states).

## How does report-only mode affect airgap.js? [#how-does-report-only-mode-affect-airgapjs]

When report-only mode is enabled, the following systems are affected:

### Deactivated systems [#deactivated-systems]

- Request & cookie protection: Disabled to allow all request and cookie mutation events through unregulated
- [Generated Content-Security-Policy protection](../concepts/content-security-policies.mdx)
- [Consent Built-In Integrations](../../index.mdx)

### Active systems [#active-systems]

- Request and cookie monitoring: Enables 100% observability of same-realm sent requests & applicable cookie mutations
- [Telemetry](../configuration/triaging-and-telemetry/telemetry-overview.mdx)
- [Telemetry false positive filtering](../configuration/triaging-and-telemetry/telemetry-configuration.mdx#telemetry-false-positive-filtering)
- [Consent management sync](../reference/sync.mdx)

## How can I customize the privacy policy link when hosting on multiple domains? [#how-can-i-customize-the-privacy-policy-link-when-hosting-on-multiple-domains]

If you are running the consent manager on multiple domains, you can customize the link to your privacy policy by setting `data-privacy-policy` on your airgap.js `<script>` tag. For example, `data-privacy-policy="https://privacy.transcend.io/"`. This [data attribute](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes) will override the value of the privacy policy link that is set in the Admin Dashboard.

This value can also be set after the script has loaded, using `transcend.setPrivacyPolicy(string)`.

The [CompleteOptionsInverted](https://github.com/transcend-io/consent-manager-ui#completeoptionsinverted) view state also uses the `data-secondary-policy` data attribute and `transcend.setSecondaryPolicy(string)` function to display a secondary policy link.

## Why can’t I see the consent banner? [#why-cant-i-see-the-consent-banner]

This could be happening for a couple of different reasons:

1\. You are not in a region with applicable data laws. For California/US residents, users are opted in by default, and websites are not required to obtain consent/show a banner. For EU citizens, users are opted out by default, which means websites must ask for consent with a banner.

**💡 Solution:** Use our [userscript here](./debugging-and-testing.mdx#testing-with-userscripts-and-simulating-consent-in-production) to simulate accessing the site from a given region (EU or US).

2\. You have already confirmed your consent. To clear out your consent, type the following into your dev console:

```javascript
addEventListener('click', airgap.reset);
// click anywhere on the page
```

## Why isn't my view state change reflected on my site? [#why-isnt-my-view-state-change-reflected-on-my-site]

There are a couple of reasons this might be happening:

1\. You haven't published your changes. After updating your configuration in the dashboard, you need to click the blue **Publish** button for the changes to take effect. If there are no pending changes shown, the dashboard configuration has already been published.

2\. Your site is hardcoding the view state in your front-end code. If you have `data-prompt="off"` on your airgap.js script tag and are manually triggering the consent UI (e.g., from a "Cookie Preferences" button), the view state configured in the dashboard has no effect. Your code may be calling something like:

```javascript
airgap.showConsentManager({ viewState: 'CompleteOptions' });
```

You'll need to update this directly in your front-end code to the desired view state, e.g.:

```javascript
airgap.showConsentManager({ viewState: 'CompleteOptionsToggles' });
```

The available view states can be found in the consent manager UI repository (https\://github.com/transcend-io/consent-manager-ui#view-states).

## How do I build my own consent manager UI? [#how-do-i-build-my-own-consent-manager-ui]

See [Creating your own UI](../configuration/consent-ui/creating-your-own-ui.mdx).

## How do I get a user's current consent? [#how-do-i-get-a-users-current-consent]

The `airgap.getConsent()` API returns an object in this format:

```typescript
interface TranscendConsentPreferences {
  /** consent to individual tracking purposes */
  purposes: {
    /**
     * true -> confirmed consent
     * false -> unconfirmed consent or opted out
     * 'Auto' -> unconfirmed consent, but opted in by default
     */
    [TrackingPurpose in string]: boolean | 'Auto';
  };
  /**
   * Was tracking consent confirmed by the user?
   * If this is false, the consent was resolved from defaults & is not yet confirmed
   **/
  confirmed: boolean;
  /**
   * Consent update/sync timestamp (ISO 8601 format)
   * e.g. "2021-06-29T19:16:13.964Z"
   */
  timestamp: string;
}
```

It's noteworthy that the value for each of the purposes can be one of 3 values:

- `true`: The website visitor explicitly confirmed consent for that purpose.
- `false`: The website visitor explicitly unconfirmed consent for that purpose, OR the website visitor was opted out by default based on your [regional experiences](https://app.transcend.io/consent-manager/regional-experiences) configuration.
- `'Auto'`: The website visitor is opted in by default based on your [regional experiences](https://app.transcend.io/consent-manager/regional-experiences) configuration. The website visitor has never explicitly confirmed consent for that purpose.

Example:

```javascript
await airgap.sync();
const { purposes } = airgap.getConsent();
console.log(JSON.stringify(purposes, null, 2));
// example log message: { Functional: true, Advertising: true, Analytics: true }
```

### Can I subscribe to consent changes with an event listener? [#can-i-subscribe-to-consent-changes-with-an-event-listener]

Absolutely! See the following example:

```javascript
airgap.addEventListener(
  'consent-change',
  ({ detail: { consent, oldConsent, changes } }) => {
    console.log('old consent:', oldConsent);
    console.log('new consent:', consent);
    console.log('consent changes:', changes);
  },
);
```

## How is consent stored? [#how-is-consent-stored]

Consent preferences are persisted in localStorage directly on the end-user's browser. If the end user has previously been on your website on that browser, then the consent preferences will be restored from their prior session.

## How do I store consent on my backend? [#how-do-i-store-consent-on-my-backend]

See [Syncing consent to your backend](../configuration/persist-consent.mdx#storing-consent-on-your-backend).

## How do I restore consent to logged-in users from my backend? [#how-do-i-restore-consent-to-logged-in-users-from-my-backend]

See [Restoring consent to users from your backend](../configuration/persist-consent.mdx#restoring-consent-to-logged-in-users-from-your-backend).

## How do I set user consent from my website UI? [#how-do-i-set-user-consent-from-my-website-ui]

Consent changes require proper authorization. See [consent authorization](../configuration/consent-ui/creating-your-own-ui.mdx#consent-authorization) to learn more.

- Option 1: Set specific tracking preferences
  **airgap.js onload**

```html
  <script
    src=".../airgap.js"
    onLoad="airgap.setConsent(event, { Functional: true, ... })"
  ></script>
```

**HTML**

```html
  <button onClick="airgap.setConsent(event, { Functional: true, ... })">
    Save tracking preferences
  </button>
```

**JavaScript**

```javascript
  saveConsentButton.addEventListener('click', (event) => {
    airgap.setConsent(event, { Functional: true /* ... */ });
  });
```

- Option 2: Opt-in or opt-out to all tracking preferences (`airgap.optIn()` and `airgap.optOut()`)
  **HTML**

```html
  <button onclick="airgap.optOut(event)">Opt-out</button>

  <button onclick="airgap.optIn(event)">Opt-in</button>
```

**JavaScript:**

```javascript
  optOutButton.addEventListener('click', airgap.optOut);

  optInButton.addEventListener('click', airgap.optIn);
```

- Set "Do not sell my personal information" tracking preference

```javascript
  // "Do not sell my personal information" consent (CCPA/CPRA)
  saleOfInfoOptOutButton.addEventListener('click', (interaction) => {
    airgap.setConsent(interaction, { SaleOfInfo: false });
  });

  saleOfInfoOptInButton.addEventListener('click', (interaction) => {
    airgap.setConsent(interaction, { SaleOfInfo: true });
  });
```

## How can I A/B test my consent manager? [#how-can-i-ab-test-my-consent-manager]

It's simple to use your A/B testing software with the consent manager, because you can create variants and track outcomes programmatically.

- To create variants, you can [show different view states programmatically](#how-do-i-display-the-built-in-consent-manager-ui) (e.g., `await transcend.showConsentManager({ viewState: myABTestVariant });`)
- To report outcomes, you can [collect consent events programmatically](../reference/individual-tracking.mdx#listening-for-consent-change-events).

## Can I use the replay quarantine without local storage? [#can-i-use-the-replay-quarantine-without-local-storage]

If you are concerned about the ePrivacy Directive and use of local storage, you can limit our replay quarantine feature to use a non-persistent in-memory queue by setting `data-replay="mutations"` on your airgap.js script tag. Our quarantine can also be completely disabled with `data-replay="off"`.

The default behavior is to persist the replay quarantine between pageviews.

Note that setting the quarantine to use non-persistent storage will cause the queue to be cleared on each page load.

## Asynchronous loading with `airgap.ready()` [#asynchronous-loading-with-airgapready]

You may load airgap.js asynchronously, though you should be aware that airgap.js can not regulate any scripts that load before it. Asynchronous loading is recommended for single-page applications in order to further reduce page load time.

```tsx
// Call `await getAirgap()` to load airgap.js

// Get your bundle ID at https://app.transcend.io/consent-manager/developer-settings
// Bundle ID is in this format: https://transcend-cdn.com/cm/{bundle-id}/airgap.js
const BUNDLE_ID = 'your-bundle-id-here';

let airgapAPI;
const getAirgap = () =>
  (airgapAPI ??= new Promise((resolve, reject) => {
    // Stub airgap.js ready queue
    if (!self?.airgap?.ready) {
      self.airgap = {
        readyQueue: [],
        ready(callback) {
          this.readyQueue.push(callback);
        },
        ...self?.airgap,
      };
    }

    // Wait for airgap.js to be ready
    self.airgap.ready((airgap) => {
      resolve(airgap);
    });

    const script = document.createElement('script');

    // Reject promise if airgap.js fails to load
    script.addEventListener('error', (evt) => {
      reject(evt);
    });

    // Specify load options:
    // e.g.
    // script.dataset.lazyLoadUi = 'on';
    // script.dataset.prompt = 'auto';
    // script.dataset.dismissedViewState = 'Closed';

    // Load airgap.js script asynchronously
    script.async = script.defer = true;
    script.src = `https://transcend-cdn.com/cm/${BUNDLE_ID}/airgap.js`;
    document.documentElement?.appendChild?.(script);
  }));
```

## Cross-domain quarantine sync [#cross-domain-quarantine-sync]

Cross-domain **consent sync** works without any special configuration. In order to also get cross-domain quarantine sync, you need to [configure a same-site sync endpoint](../configuration/hosting-options.mdx#instructions-for-self-hosting-sync-endpoints) and configure the [quarantine-sync-budget](../reference/airgap/load-options.mdx#quarantine-sync-budget) load option. If you are using a Transcend-hosted sync endpoint, reach out to support to configure this option.

## How do I get a user's current consent without airgap.js? [#how-do-i-get-a-users-current-consent-without-airgapjs]

This is possible through our cross-domain iframe postMessage interface, [Transcend XDI](../reference/xdi.mdx).

This API is automatically scoped to only allow the domains in your domain list to get consent and quarantine data. By default, private quarantine data synchronized through this API never leaves the browser.

```javascript
// Get your bundle ID at https://app.transcend.io/consent-manager/developer-settings
// Bundle ID is in this format: https://transcend-cdn.com/cm/{bundle-id}/airgap.js
const BUNDLE_ID = 'your-bundle-id-here';

let xdiAPI;
const getXDI = () =>
  (xdiAPI ??= new Promise((resolve, reject) => {
    // Stub Transcend XDI ready queue
    if (!self?.transcend?.xdi?.ready) {
      self.transcend = {
        ...self?.transcend,
        xdi: {
          readyQueue: [],
          ready(callback) {
            this.readyQueue.push(callback);
          },
          ...self?.transcend?.xdi,
        },
      };
    }

    // Wait for Transcend XDI to be ready
    self.transcend.xdi.ready((xdi) => {
      resolve(xdi);
    });

    const script = document.createElementNS(
      'http://www.w3.org/1999/xhtml',
      'script',
    );
    script.addEventListener('error', (evt) => {
      reject(evt);
    });

    // Load Transcend XDI script asynchronously
    script.async = script.defer = true;
    script.src = `https://transcend-cdn.com/cm/${BUNDLE_ID}/xdi.js`;
    document.documentElement.appendChild(script);
  }));

let xdiChannel;
const connect = async () =>
  (xdiChannel ??= await new Promise(async (resolve) => {
    const xdi = await getXDI();
    resolve(
      await xdi.connect(
        `https://${YOUR_SYNC_ENDPOINT}.com/consent-manager/${BUNDLE_ID}`,
      ),
    );
  }));

const getConsent = async () => {
  const channel = await connect();
  const { consent } = await channel.run('ConsentManager:Sync', {
    sync: ['consent'],
  });
  return consent;
};

console.log(await getConsent());
```

## Can I conditionally load airgap.js asynchronously for California residents only? [#can-i-conditionally-load-airgapjs-asynchronously-for-california-residents-only]

Absolutely. With this snippet, you're ready to go!

```javascript
// Get your bundle ID at https://app.transcend.io/consent-manager/developer-settings
// Bundle ID is in this format: https://transcend-cdn.com/cm/{bundle-id}/airgap.js
const isCaliforniaResident =
  self.Intl &&
  new Intl.DateTimeFormat().resolvedOptions().timeZone ===
    'America/Los_Angeles';

const doSomethingThatRequiresFacebookLDUOverrides = async () => {
  if (isCaliforniaResident) {
    // Wait for airgap.js to init
    // This automatically starts applying our global LDU/etc. overrides
    await getAirgap();
  }
  // loadFacebookPixel(...)
};
```

See the [airgap.js API reference](../reference/airgap/api.mdx) for more API information.

## Feature Regulation Roadmap [#feature-regulation-roadmap]

We fully regulate almost every API used for tracking in modern browsers. Notably absent from our first-class regulatory coverage are the following technologies, all of which are on our roadmap with clear paths forward to add regulation support:

- CSS / CSSOM
- SVG-namespaced subresources
- ECMAScript ShadowRealms API
- Trusted Types override/quarantine functionality — Trusted Types-originating requests can be allowed/denied but not yet overridden in a seamless manner.

Note: All of these technologies **are regulated with blocking** when using a strict [Content Security Policy](../concepts/content-security-policies.mdx). Using a lax CSP will only cover regulation capability gaps for unknown/untriaged data flows, as the generated CSP will allow all approved data flows. You can enable our dynamic Content Security Policy feature in your [Consent Management dashboard](https://app.transcend.io/consent-manager).

### Regulating SVG and CSS trackers [#regulating-svg-and-css-trackers]

If your site includes any SVG or CSS trackers, you should use a strict [Content Security Policy](../concepts/content-security-policies.mdx) to ensure that these trackers are correctly regulated.

### Regulating automated page navigation [#regulating-automated-page-navigation]

Automated page navigation can be regulated by setting `data-regulate-navigation="on"` on your airgap.js script tag. This feature requires browser support for the Navigation API, which is currently only supported by Google Chrome. On browsers that do not support the Navigation API, this feature will have no effect.

## What is the performance overhead incurred by airgap.js? [#what-is-the-performance-overhead-incurred-by-airgapjs]

Transcend Consent Management was designed to load quickly and operate efficiently with minimal impact to the website experience. Check out Transcend's blog post on [Architecting Transcend Consent Management for performance](https://transcend.io/blog/architecting-transcend-consent-performance/) for more information on how airgap.js was built with speed and efficiency in mind.

The following performance metrics were measured on an Apple M1 Pro laptop under light-medium load, using the transcend.io bundle configuration:

- Request processing overhead
  - Average (hot JIT): \~35μs/request
  - Worst case (cold JIT): \~350μs/request
  - This is the entire overhead for 'pure network APIs' (e.g. `fetch`, `XMLHttpRequest`, etc.)
- 'HTML string' DOM mutation processing overhead: \~65μs/request-causing node (in addition to request processing overhead)
  - HTML string consumers include `innerHTML`, `outerHTML`, `iframe.srcdoc`, and `document.write`

As a stress test, we benchmarked re-processing transcend.io's entire homepage (equivalent to calling `document.documentElement.innerHTML = document.documentElement.innerHTML`) and measured an average total latency of 24.6ms while processing \~250 request-causing elements per call.

Note: Performance overhead scales linearly with the size of your bundle configuration.

## How large is Transcend Consent Management? [#how-large-is-transcend-consent-management]

Currently, as of April 3rd, 2025 our uncompressed sizes are as follows:

- airgap.js core: 113.952 KB (can be [loaded asynchronously](#asynchronous-loading-with-airgapready))
- UI: 342.536 KB (async and optionally lazy-loaded)
- Private cross-domain sync host (XDI): 23.741 KB (lazy-loaded)

## How does caching work on the Transcend CDN? [#how-does-caching-work-on-the-transcend-cdn]

We currently do not configure any browser-caching for our Consent JS files (`https://transcend-cdn.com/cm/...`). The only caching that happens is DNS caching + CDN caching. This simplifies publishing changes to your consent manager bundle, but could result in slower load times for users on low bandwidth.

Going forward, our plan is to configure browser-caching for 4 hours by default, and allow each customer to configure this time period from the Admin Dashboard. When initially setting up the bundle, customers can set this time period to 0 (disabling browser cache), and then increase the time period as the Consent bundle stabilizes and no longer need to be updated frequently.

Even with CDN caching enabled, it is possible to generate a dynamic query parameter that can be used to cache-bust according to your own set of criteria. For example, you could create a cache-buster to re-fetch the script once per hour, even if the CDN caching was configured to 4 hours.

```javascript
function roundToHour(date) {
  p = 60 * 60 * 1000; // milliseconds in an hour
  return new Date(Math.round(date.getTime() / p) * p);
}
const cacheString = roundToHour(new Date()).toISOString();

const script = document.createElement('script');

script.setAttribute(
  'src',
  'https://transcend-cdn.com/cm/ee571c7f-030a-41b2-affa-70df8a47b57b/airgap.js?=' +
    cacheString,
);
document.head.appendChild(script);
```

An alternative method that we can also support is to have separate paths, one for versioned files and one that always has the latest version. When using the latest version/static URL, you can cache for some short period of time (or have no cache). When using the versioned URL, you can cache indefinitely and “cache-bust” by updating the URL to point to the newer hash/version.

## How can I show a banner whenever my Privacy Policy is updated? [#how-can-i-show-a-banner-whenever-my-privacy-policy-is-updated]

You can store a flag on the user's machine that will check if the banner was shown for each user, with the following code:

```javascript
const hasSeenNotice =
  localStorage.getItem('privacyPolicyNotice15-1-2024') === 'true';
if (!hasSeenNotice) {
  transcend.showConsentManager({ viewState: 'PrivacyPolicyNotice' });
  localStorage.setItem('privacyPolicyNotice15-1-2024', 'true');
}
```

You can change and customize which [View State](https://github.com/transcend-io/consent-manager-ui?tab=readme-ov-file#view-states) to show, in this example `PrivacyPolicyNotice`.
You can also update your code with the key describing each new update requiring the display of the banner, in the above example `privacyPolicyNotice15-1-2024`.

## Can Tag Managers be used to inject Transcend Consent Management script? [#can-tag-managers-be-used-to-inject-transcend-consent-management-script]

The safest/recommended approach is still to include the script directly into the HTML header. But if that is not feasible, then you can also use Tag Managers to inject the script. When using a tag manager, it is important to configure the airgap.js script to be the first script injected. This allows the airgap.js script to regulate subsequent scripts that get injected.

Read on for more specific instructions.

### Google Tag Manager (GTM): [#google-tag-manager-gtm]

The GTM script itself should be safe to load without having any tracking side effects, and you can use a GTM template to load airgap.js. For instructions on how to do so, please refer to our detailed guide on setting up Consent for [Google Consent Mode](../configuration/consent-integrations/google-consent-mode.mdx).

References:

- [Consent Initialization](https://support.google.com/tagmanager/answer/7679319)

### Adobe [#adobe]

[Adobe Tag Manager](https://experienceleague.adobe.com/docs/experience-platform/tags/home.html?lang=en) can be used to inject airgap.js by configuring Rule Ordering such that airgap.js can be the first script to load. Rules have an “order priority” number where lower numbered rules fire first, so you can create a new Rule to load airgap.js with the priority ordering of “0” and event type of “Core - Library Loaded”. You’ll also need to do an audit of all other rules configured to make sure that there is no other script which has a lower order priority than airgap, as rules with the same order run in no particular order.

However, even with this configuration, it’s possible that a rule will fire out of order, because Adobe does not offer any guarantees around how extensions are implemented. They have this call-out in their [docs](https://experienceleague.adobe.com/docs/experience-platform/tags/ui/rules.html?lang=en#rule-ordering): “Ultimately, the responsibility for executing actions in order lie with the extension developer of the event type that you are using. Adobe extension developers ensure their extensions work as intended. Adobe provides guidance to third-party extension developers to do this properly, but cannot guarantee how these guidelines are followed.” So the only way to ensure that things work as expected is to test thoroughly and to have continuous monitoring of what tools are running on your site (which is provided through our <GlossaryTerm id="telemetry">telemetry</GlossaryTerm> and dashboards).

References:

- [Rule Load events](https://experienceleague.adobe.com/docs/experience-platform/tags/extensions/adobe/core/overview.html?lang=en#page-load-events)
- [Rule Ordering](https://experienceleague.adobe.com/docs/experience-platform/tags/ui/rules.html?lang=en#rule-ordering)

### Tealium [#tealium]

The initial request to fetch the Tealium script manager should not be tracking user data as a side effect. To have Transcend Consent Management load as early as possible, we recommend adding it to the `utag.sync.js` script. The script is placed in the `<head>` section of your page code and loads synchronously to comply with the most common vendor requirements. Instructions for how to do this can be found here. You'll want to modify the `utag.sync.js` script to load our Consent script as follows:

```javascript
// Get your bundle ID at https://app.transcend.io/consent-manager/developer-settings
// Bundle ID is in this format: https://transcend-cdn.com/cm/{bundle-id}/airgap.js
const script = document.createElement('script');

// Specify load options:
// e.g.
// script.dataset.lazyLoadUi = 'on';
// script.dataset.prompt = 'auto';
// script.dataset.dismissedViewState = 'Closed';

// Load airgap.js script synchronously
script.src = `https://transcend-cdn.com/cm/${BUNDLE_ID}/airgap.js`;
document.documentElement?.appendChild?.(script);
```

References:

- [Using utag.sync.js](https://community.tealiumiq.com/t5/iQ-Tag-Management/Using-the-utag-sync-js-Script/ta-p/19175#toc-hId-441930825)

## Can Transcend Consent Management be used inside sandboxed iframes? [#can-transcend-consent-management-be-used-inside-sandboxed-iframes]

Sandboxed iframes can prevent cross-subdomain sync from completing unless the [sandbox attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox) includes both the `allow-scripts` and `allow-same-origin` directives. Cookie regulation and cookie-based sync also require both of these directives, and can be disabled by setting `data-regulate-cookies="off"` and `data-local-sync="private-only"` on your airgap.js script tag. All other functionality only requires the `allow-scripts` directive at a minimum.
