await transcend.showConsentManager()

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 to simulate accessing the site from a given region (EU or US).

  1. You have already confirmed your consent. To clear out your consent, type the following into your dev console:
addEventListener('click', airgap.reset);
// click anywhere on the page

See Creating your own UI.

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

"purposes": {
// consent to individual tracking purposes
[TrackingPurpose in string]: boolean;
// 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)
"timestamp": string; // e.g. "2021-06-29T19:16:13.964Z"


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

Absolutely! See the following example:

({ detail: { consent, oldConsent, changes } }) => {
console.log('old consent:', oldConsent);
console.log('new consent:', consent);
console.log('consent changes:', changes);

See Syncing consent to your backend.

See Restoring consent to users from your backend.

Consent changes require proper authorization. See consent authorization to learn more.

  • Option 1: Set specific tracking preferences airgap.js onload

    onLoad="airgap.setConsent(event, { Functional: true, ... })"


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


    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

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


    optOutButton.addEventListener('click', airgap.optOut);
    optInButton.addEventListener('click', airgap.optIn);
  • Set "Do not sell my personal information" tracking preference

    // "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 });

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.

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.

// Call `await getAirgap()` to load airgap.js
// Get your bundle ID at https://app.transcend.io/consent-manager#GeneralSettings
// Bundle ID is in this format: https://cdn.transcend.io/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) {
// Wait for airgap.js to be ready
self.airgap.ready((airgap) => {
const script = document.createElement('script');
// Reject promise if airgap.js fails to load
script.addEventListener('error', (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://cdn.transcend.io/cm/${BUNDLE_ID}/airgap.js`;

Cross-domain consent sync works without any special configuration. In order to also get cross-domain quarantine sync, you need to host your own first-party sync endpoint.

This is a very easy two-step process:

  1. Host a publicly-accessible static page on your site with this HTML, replacing the value of data-first-party with a space-separated list of domains that you wish to sync between.
<!DOCTYPE html>
data-first-party="your-site.example your-other-site.example"
  1. Update your Consent Manager script as such. Take note to enter the URL to the page you created in step 1 in the data-sync-endpoint attribute.

Our consent manager uses Transcend XDI to facilitate consent & quarantine sync. This interface works by embedding iframes to known sync endpoints in order to construct secure offline tunnels for transferring sync data.

  • Your Consent Manager's allowed domains list must include your Privacy Center domain. This is already true if your Privacy Center is on a subdomain of your primary domain (e.g. privacy.your-site.example)
  • Your sync endpoint's embedder policy must allow embedding by your Privacy Center.
  • Sync clients must connect from secure HTTPS origins.

This is possible through our cross-domain iframe postMessage interface, Transcend XDI.

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.

// Get your bundle ID at https://app.transcend.io/consent-manager#GeneralSettings
// Bundle ID is in this format: https://cdn.transcend.io/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 = {
xdi: {
readyQueue: [],
ready(callback) {
// Wait for Transcend XDI to be ready
self.transcend.xdi.ready((xdi) => {
const script = document.createElementNS(
script.addEventListener('error', (evt) => { reject(evt) });
// Load Transcend XDI script asynchronously
script.async = script.defer = true;
script.src = `https://cdn.transcend.io/cm/${BUNDLE_ID}/xdi.js`;
let xdiChannel;
const connect = async () =>
(xdiChannel ??= await new Promise(async (resolve) => {
const xdi = await getXDI();
await xdi.connect(
const getConsent = async () => {
const channel = await connect();
const { consent } = await channel.run('ConsentManager:Sync', {
sync: ['consent']
return consent;
console.log(await getConsent());

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

// Get your bundle ID at https://app.transcend.io/consent-manager#GeneralSettings
// Bundle ID is in this format: https://cdn.transcend.io/cm/{bundle-id}/airgap.js
const isCaliforniaResident =
self.Intl &&
new Intl.DateTimeFormat().resolvedOptions().timeZone ===
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 for more API information.

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:

  • SVG-namespaced subresources
  • Realms
    • Same-origin frame (w/o our Consent Manager) subresources
    • ECMAScript ShadowRealms API
  • Automated page navigation (e.g. loading a new page without responding to a user gesture)
  • Trusted Types override/quarantine functionality — Trusted Types-originating requests can be allowed/denied but not yet overridden or quarantined in a seamless manner.

Note: All of these technologies (except automated page navigation) are regulated with blocking when using a Content Security Policy. You can enable our dynamic Content Security Policy feature in your Consent Manager Admin Dashboard.

Currently, as of February 17, 2022 our uncompressed sizes are as follows:

  • airgap.js core: 33 KB compressed / 83 KB uncompressed (can be loaded asynchronously)
  • UI: 131 KB compressed / 698 KB uncompressed (async and optionally lazy-loaded)
  • Private cross-domain sync host (XDI): 10 KB compressed / 21 KB uncompressed (lazy-loaded)

📏 Size changes are coming soon! The following changes will happen later in development.

These are estimates of the size changes that will happen with our upcoming changes:

  • +5-10 KB (uncompressed) to airgap.js core to regulate the remaining technologies on our Regulation Roadmap

  • -7-10 KB (uncompressed) from all of airgap.js core and XDI through clearly attainable optimizations in common components

  • -300 KB (uncompressed) from future UI refactors

Note: Size figures do not include additional embedded configuration data.