Data Layer variables in Project ad slots

In this example, you will learn how to pass Data Layer variables from your website into the ad iframe inside a Riddle. The most common use case is passing consent from your CMP (Consent Management Platform) to Google Ad Manager (GAM) so that your ads on Riddle behave correctly under GDPR, TCF, or your custom consent setup. The same pattern works for any other Data Layer variable you want to forward to your ad server (e.g. audience segments, page categories, user IDs).

Important – read this first. The consent comes from your website (e.g. mydomain.com), where your CMP lives. The Riddle is embedded on your page as an iframe, and the GAM ad tag runs inside a second iframe nested inside the Riddle. Because the ad is two iframe boundaries away from your CMP, the value has to be passed in via postMessage – there is no way for the ad code to read your CMP directly. This is the part most customers get wrong, and it's why the setup below uses both a Data Layer push and a consent handshake.

How it works

Why this is more complex than a normal CMP-to-GAM integration

The most important thing to understand – and the part that most customers get wrong – is where the consent lives and where the ad runs:

  • The CMP runs on your website (e.g. mydomain.com). It knows whether the visitor accepted or rejected cookies. Only your page has this information.
  • The Riddle is embedded on your website as an iframe that loads riddle.com. Your CMP cannot reach inside this iframe directly because of cross-origin browser security.
  • The GAM ad tag runs in a second iframe inside the Riddle iframe. Each project ad slot is its own sandboxed iframe. The ad iframe is therefore two boundaries away from your CMP.
┌──────────────────────────────────────────────────────────────────┐
│  mydomain.com (your website)                                     │
│  ─────────────────────────                                       │
│  Your CMP lives here. It knows the consent state.                │
│                                                                  │
│   ┌────────────────────────────────────────────────────────┐     │
│   │  Riddle iframe (riddle.com)                            │     │
│   │  ─────────────────────────                             │     │
│   │  The Riddle quiz/poll renders here.                    │     │
│   │                                                        │     │
│   │   ┌──────────────────────────────────────────────┐     │     │
│   │   │  Ad iframe (your AdServer Tag code)          │     │     │
│   │   │  ───────────────────────────────────         │     │     │
│   │   │  GPT loads, calls GAM, renders the creative. │     │     │
│   │   │  Needs the consent value to set targeting.   │     │     │
│   │   └──────────────────────────────────────────────┘     │     │
│   │                                                        │     │
│   └────────────────────────────────────────────────────────┘     │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

Because the consent value has to cross two iframe boundaries to reach GAM, the integration uses window.postMessage and a small handshake. You can't just read a cookie or call a CMP API from inside the ad iframe – the ad iframe is on a different origin and can't see your CMP at all.

The three pieces of code

  1. The host page (mydomain.com) – has the CMP and embeds the Riddle. It pushes the consent state into the Riddle Data Layer and answers consent requests coming from the ad iframe.
  2. The Riddle Data Layer variable – defined under Publish → Data Layer inside the Riddle Creator. This is the named channel (e.g. consent) that Riddle uses to forward values from the host page through to every iframe it controls, including the ad iframe.
  3. The ad iframe – the AdServer Tag code you paste into a Riddle project ad slot. It listens for the consent value, sets a GAM key-value targeting on it, and refreshes the GAM slot so the right creative is served.

The two postMessage channels

The communication uses two postMessage message types, both of which traverse the nested iframes:

  • isRiddleDataLayerEvent – fired by the Riddle embed whenever a Data Layer entry changes on the host page. The payload contains a riddleDataLayer array of { key, value } pairs. Riddle forwards this message into every ad iframe it has rendered, so your ad code receives the consent value automatically.
  • riddleConsentRequest / riddleConsentReply – a dedicated consent handshake. These are not built into the Riddle product – they are a convention used by this example. The ad iframe (Code part 2) posts a riddleConsentRequest up to window.top, and your host page script (Code part 1) listens for it and replies with riddleConsentReply carrying the current consent value. Riddle itself does not intercept or respond to these messages – they pass straight through the Riddle iframe to the top window.
    The handshake exists because the ad iframe loads asynchronously and might initialise before or after the CMP has decided. Without it, the ad code would have to wait for the next Data Layer push to get any value, leaving the first ad call unrouted.

The recommended pattern uses both channels: the iframe asks for consent on load via the handshake, sets the GAM targeting key, refreshes the slot, and then keeps listening for further isRiddleDataLayerEvent updates so the targeting can be re-applied if the user changes their consent choice later (e.g. opens the CMP banner again and revokes consent).

Setup

Step 1 – Define the Data Layer variable

Inside the Riddle Creator, open your Riddle and go to Publish → Data Layer (see the Data Layer help page for screenshots). Add a Data Layer item with the key consent (or any other key you want to forward).

Step 2 – Add an ad slot to your project

Go to Your projects → select your project → Ad Slots. Use one of the ad slots and paste the ad iframe code (further down on this page) into the AdServer Tag / Code field.

Step 3 – Select the ad slot in your Riddle

Go to your Riddle → Settings → Monetization: Display ads and select the ad slot you just configured.

Step 4 – Wire the host page to your CMP

On the page where the Riddle is embedded, add the host page script (further down on this page) and connect it to your CMP. The script does two things:

  • pushes the current consent state into window.riddleDataLayer whenever the CMP value changes;
  • replies to riddleConsentRequest messages from the ad iframe with the current value.

If you are new to the Riddle Data Layer, the Push website data into a Riddle example shows the general push pattern without the ad-specific parts.

Example Riddle with project ad slot

Variable content

Code part 1: the host page

This code lives on your own website – the page that embeds the Riddle (e.g. mydomain.com). It is the only place that has direct access to your CMP, so it acts as the bridge between the CMP and everything inside the Riddle iframe.

It needs to do two things: keep window.riddleDataLayer in sync with the current CMP value, and respond to consent requests coming from the ad iframe nested inside the Riddle.

The riddleDataLayer is just a global array on window – any time you push() a { key, value } object into it, the Riddle embed picks it up and forwards it to the ad iframe as an isRiddleDataLayerEvent. That means you can call window.riddleDataLayer.push(...) from anywhere on your page, including from your CMP's callbacks.

Placeholders to replace:

  • YOUR_RIDDLE_ID – the ID of the Riddle you want to embed (you'll get the full embed snippet from Share → Embed Code in the Riddle Creator)
  • The document.getElementById(...) button handlers at the bottom of the script are demo wiring that lets you click "Give Consent" / "Deny Consent" to test the flow. In production, your CMP hooks replace them – e.g. a CMP callback, an event listener on a TCF API, or a watcher on a __tcfapi / OneTrust / Cookiebot / Usercentrics global (see the commented examples inside the code).
<html>
<head>
  <title>Riddle with consent-aware ad</title>
</head>
<body>
  <h1>Riddle with banner ad</h1>
  <p>The ad will select a creative depending on the cookie consent given or not given.</p>

  <!-- Riddle embed - replace YOUR_RIDDLE_ID with your actual Riddle ID -->
  <div class="riddle2-wrapper"
       data-rid-id="YOUR_RIDDLE_ID"
       data-auto-scroll="true"
       data-is-fixed-height-enabled="false"
       data-bg="#fff"
       data-fg="#00205b"
       style="margin:0 auto; max-width:100%; width:640px;">
    <script src="https://www.riddle.com/embed/build-embedjs/embedV2.js"></script>
    <iframe title="Riddle"
            src="https://www.riddle.com/embed/a/YOUR_RIDDLE_ID?lazyImages=false&staticHeight=false"
            allow="autoplay"
            referrerpolicy="strict-origin"></iframe>
  </div>

  <!--
    Demo-only buttons. Remove these in production.
    They simulate the user accepting or rejecting cookies in your CMP.
  -->
  <div class="consent-buttons">
    <button type="button" id="giveConsentBtn">Give Consent</button>
    <button type="button" id="denyConsentBtn">Deny Consent</button>
  </div>

  <script>
    // riddleDataLayer must exist BEFORE the Riddle embed script reads it.
    // It's a global queue: anything you push() into it is forwarded to the Riddle.
    window.riddleDataLayer = window.riddleDataLayer || [];

    /**
     * Push the current consent state into the Riddle Data Layer.
     * Call this from your CMP whenever the consent decision is made or changes.
     *
     * @param {'granted'|'denied'} value
     */
    function pushConsentToRiddle(value) {
      window.riddleDataLayer.push({ key: 'consent', value: value });
    }

    // -----------------------------------------------------------------
    // CMP INTEGRATION POINT
    // -----------------------------------------------------------------
    // Replace the two button listeners below with your real CMP hook.
    // Examples:
    //
    //   // OneTrust
    //   window.OneTrust.OnConsentChanged(function () {
    //     var hasAdConsent = OnetrustActiveGroups.indexOf('C0004') !== -1;
    //     pushConsentToRiddle(hasAdConsent ? 'granted' : 'denied');
    //   });
    //
    //   // IAB TCF v2
    //   __tcfapi('addEventListener', 2, function (tcData, success) {
    //     if (!success) return;
    //     var hasAdConsent = tcData.purpose.consents[1] && tcData.purpose.consents[3];
    //     pushConsentToRiddle(hasAdConsent ? 'granted' : 'denied');
    //   });
    //
    //   // Cookiebot
    //   window.addEventListener('CookiebotOnAccept', function () {
    //     pushConsentToRiddle(Cookiebot.consent.marketing ? 'granted' : 'denied');
    //   });
    //
    //   // Usercentrics
    //   window.addEventListener('UC_UI_INITIALIZED', function () {
    //     var status = UC_UI.getServicesBaseInfo(); // inspect for ad-relevant services
    //     pushConsentToRiddle(/* derive granted|denied */);
    //   });
    // -----------------------------------------------------------------

    // Demo wiring (remove in production):
    document.getElementById('giveConsentBtn').addEventListener('click', function () {
      pushConsentToRiddle('granted');
    });
    document.getElementById('denyConsentBtn').addEventListener('click', function () {
      pushConsentToRiddle('denied');
    });

    // Consent handshake:
    // The ad iframe inside the Riddle posts { type: 'riddleConsentRequest' } on load,
    // because it needs an answer immediately and can't wait for the next push.
    // We reply with the most recent value from window.riddleDataLayer.
    window.addEventListener('message', function (event) {
      if (!event.data || event.data.type !== 'riddleConsentRequest') return;

      var current = (window.riddleDataLayer || []).find(function (i) {
        return i.key === 'consent';
      });
      var value = current ? current.value : 'denied'; // default to denied if CMP hasn't fired yet

      event.source.postMessage({
        type: 'riddleConsentReply',
        consent: value
      }, '*');
    });
  </script>
</body>
</html>

What this code does

  1. Initialises window.riddleDataLayer as an empty array before anything else. The Riddle embed script will read from this array, so it must exist first.
  2. pushConsentToRiddle(value) is the single function your CMP calls. It pushes { key: 'consent', value: 'granted' | 'denied' } into the Data Layer, which Riddle forwards to the ad iframe.
  3. CMP integration point – the commented examples show how to wire OneTrust, IAB TCF v2, Cookiebot, and Usercentrics. Pick whichever applies to your setup and remove the demo buttons.
  4. Consent handshake – listens for riddleConsentRequest from the ad iframe and replies synchronously with the current value (defaulting to denied if the CMP hasn't reported in yet). This is what lets the iframe fire its first ad call without waiting.

Code part 2: the ad iframe (project ad slot)

This is the ad tag that goes into your Riddle project ad slot. It runs two iframe boundaries away from your CMP – inside an iframe that Riddle creates for each ad placement, which itself sits inside the Riddle iframe on your website. Because of that, it cannot read cookies from your domain or call your CMP API directly. Instead, it receives the consent value via postMessage from the host page (Code part 1), sets a GAM key-value targeting on it, and refreshes the slot so GAM serves the right creative.

Scope of this example: the code below is built for single-size slots – a fixed [width, height] like [600, 600] or [300, 250]. The scaleAd() mobile-fit logic and the hard-coded [AD_WIDTH, AD_HEIGHT] size in defineSlot both assume one creative size. If you need a multi-size or responsive slot (e.g. [[300, 250], [336, 280]]), you will have to remove or rework the scaling logic and use defineSizeMapping() instead – that pattern is out of scope here.

Paste it into the AdServer Tag / Code field of your project ad slot, then replace the placeholders.

Placeholders to replace:

  • YOUR_GAM_NETWORK_CODE – your Google Ad Manager network code (the number after / in your ad unit paths)
  • YOUR_AD_UNIT_NAME – the name of the ad unit you defined in GAM
  • AD_WIDTH / AD_HEIGHT – the creative size in pixels (the example uses 600×600)
<style>
  html, body {
    margin: 0;
    padding: 0;
    overflow: hidden;
    width: 100%;
  }
  #div-gpt-ad {
    width: AD_WIDTHpx;
    height: AD_HEIGHTpx;
    transform-origin: top left;
  }
  #div-gpt-ad iframe {
    width: AD_WIDTHpx !important;
    height: AD_HEIGHTpx !important;
  }
</style>
<script>
  window.googletag = window.googletag || { cmd: [] };
  var consentResolved = false;
  var adSlot = null; // populated inside googletag.cmd.push below

  function scaleAd() {
    var container = document.getElementById('div-gpt-ad');
    if (!container) return;
    var availableWidth = document.documentElement.clientWidth;
    if (availableWidth < AD_WIDTH) {
      var scale = availableWidth / AD_WIDTH;
      container.style.transform = 'scale(' + scale + ')';
      document.body.style.height = Math.round(AD_HEIGHT * scale) + 'px';
    } else {
      container.style.transform = 'none';
      document.body.style.height = 'AD_HEIGHTpx';
    }
  }

  function fireAd(value) {
    if (consentResolved) return;
    consentResolved = true;
    var consentValue = (value === 'granted') ? 'granted' : 'denied';
    googletag.cmd.push(function () {
      googletag.pubads().setTargeting('consent', consentValue);
      // Refresh only our own slot, not every slot on the page.
      googletag.pubads().refresh(adSlot ? [adSlot] : undefined);
      setTimeout(function() { scaleAd(); sendMessage(); }, 300);
    });
  }

  function sendMessage() {
    window.parent.postMessage({ "ad-iframe-bottom": document.body.offsetHeight }, "*");
  }

  window.addEventListener('message', function(event) {
    if (!event.data) return;

    // 1. Riddle Data Layer event - fires whenever a Data Layer variable changes
    if (event.data.isRiddleDataLayerEvent) {
      var item = event.data.riddleDataLayer.find(function(f) { return f.key === 'consent'; });
      if (item) {
        if (!consentResolved) {
          fireAd(item.value);
        } else {
          // Consent changed after the first ad call - update GAM targeting and refresh our slot
          googletag.cmd.push(function () {
            googletag.pubads().setTargeting('consent', item.value === 'granted' ? 'granted' : 'denied');
            googletag.pubads().refresh(adSlot ? [adSlot] : undefined);
          });
        }
      }
      return;
    }

    // 2. Dedicated consent reply from the parent window
    if (event.data.type === 'riddleConsentReply') {
      fireAd(event.data.consent);
    }
  });

  (function () {
    // Start the fallback timer immediately, BEFORE GPT loads.
    // If it lived inside googletag.cmd.push, the 500ms would only start
    // counting after GPT had finished loading - which on slow connections
    // is itself longer than 500ms, defeating the purpose of the fallback.
    setTimeout(function () { fireAd('denied'); }, 500);

    // Ask the parent window for the current consent state.
    // This also happens before GPT load so the reply can arrive as early as possible.
    window.top.postMessage({ type: 'riddleConsentRequest' }, '*');

    // Load GPT
    var gptScript = document.createElement('script');
    gptScript.async = true;
    gptScript.src = 'https://securepubads.g.doubleclick.net/tag/js/gpt.js';
    document.head.appendChild(gptScript);

    googletag.cmd.push(function () {
      adSlot = googletag
        .defineSlot('/YOUR_GAM_NETWORK_CODE/YOUR_AD_UNIT_NAME', [AD_WIDTH, AD_HEIGHT], 'div-gpt-ad')
        .addService(googletag.pubads());
      googletag.pubads().disableInitialLoad();
      googletag.pubads().enableSingleRequest();
      googletag.enableServices();
      googletag.display('div-gpt-ad');
    });

    scaleAd();
    sendMessage();

    var height = document.body.offsetHeight;
    setInterval(function () {
      scaleAd();
      if (height !== document.body.offsetHeight) {
        sendMessage();
        height = document.body.offsetHeight;
      }
    }, 100);
  })();
</script>
<div id="div-gpt-ad"></div>

What this code does, step by step

  1. Starts the 500ms fallback timer immediately. The timer is set before GPT loads, so the 500ms is measured from the moment the iframe runs – not from the moment GPT finishes downloading. If the timer lived inside googletag.cmd.push, it would only start counting after GPT had loaded, and on slow connections GPT alone can take longer than 500ms, defeating the purpose of the fallback.
  2. Sends a riddleConsentRequest to the parent window. Also fired before GPT loads. If your host page is wired up to your CMP, it replies with { type: 'riddleConsentReply', consent: 'granted' | 'denied' }.
  3. Loads GPT and defines the slot. disableInitialLoad() prevents GPT from firing an ad call before we know the consent state. The defined slot reference is captured in adSlot so we can refresh it specifically later.
  4. Listens for the Riddle Data Layer. In parallel, the iframe listens for isRiddleDataLayerEvent messages. If you push a consent variable into the Data Layer from your website (or from inside the Riddle), it is picked up here.
  5. Fires the ad once. Whichever signal arrives first (consent reply, Data Layer push, or the 500ms fallback) triggers fireAd(), which sets a consent key-value on googletag.pubads().setTargeting() and calls refresh([adSlot]). Passing the slot explicitly means we only refresh our slot, not every GPT slot on the page – important if you ever run this code alongside other ad tags. The consentResolved flag prevents a duplicate ad call.
  6. Handles consent changes after the fact. If the user later changes their consent choice and you push a new value into the Data Layer, the iframe updates the GAM targeting and calls refresh([adSlot]) again so the next ad respects the new consent state.
  7. Scales the creative on small viewports. scaleAd() keeps a fixed-size creative readable on mobile by applying a CSS transform, and sendMessage() reports the iframe height back to Riddle so the embed can resize.

Targeting in GAM

In your Google Ad Manager UI, define a key-value called consent with the values granted and denied, then use it in your line item targeting (e.g. only serve programmatic demand when consent=granted, serve house ads when consent=denied).


Secondary example: switching between multiple ad managers

If you don't need consent handoff and just want to swap ad creatives based on a Data Layer value, here is a simpler pattern. It listens for a Data Layer variable called ad-manager and renders a different placeholder based on its value (ad-manager-1, ad-manager-2, or anything else falls back to default). Replace the insertDummyAd body with your actual ad tag for each branch.

<script>
  function riddleDataLayerListener(event) {
    if (!event.data.isRiddleDataLayerEvent) {
      return;
    }

    var adManager = event.data.riddleDataLayer.find(
      function (f) { return f.key === "ad-manager"; }
    );

    var adManagerValue = adManager ? adManager.value : "default";

    if (adManagerValue === "ad-manager-1") {
      // code for ad manager 1
      insertDummyAd(adManagerValue);
    } else if (adManagerValue === "ad-manager-2") {
      // code for ad manager 2
      insertDummyAd(adManagerValue);
    } else {
      // code for default ad manager
      insertDummyAd("default ad manager");
    }
  }

  function sendMessage() {
    window.parent.postMessage(
      { "ad-iframe-bottom": document.body.offsetHeight },
      "*"
    );
  }

  function insertDummyAd(adManager) {
    // remove any existing ad
    var ads = document.querySelectorAll("div");
    ads.forEach(function (ad) { ad.remove(); });

    var ad = document.createElement("div");
    ad.style.width = "100%";
    ad.style.height = "100px";
    ad.style.backgroundColor = "lightgray";
    ad.style.textAlign = "center";
    ad.style.lineHeight = "100px";
    ad.style.fontWeight = "bold";
    ad.style.color = "white";
    ad.style.fontSize = "24px";
    ad.innerHTML = "Ad from " + adManager;
    document.body.appendChild(ad);
  }

  (function () {
    insertDummyAd("default ad manager");
    sendMessage();

    window.addEventListener("message", riddleDataLayerListener);

    var height = document.body.offsetHeight;
    setInterval(function () {
      if (height != document.body.offsetHeight) {
        sendMessage();
        height = document.body.offsetHeight;
      }
    }, 100);
  })();
</script>

This pattern is useful for simple A/B tests, swapping house ads based on a page category, or routing to different ad networks per Riddle. For anything involving real money or consent, use the GAM example above.