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

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.

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.