← All Guides

Iframe Tracking with Google Tag Manager: postMessage Listeners, GTM Inside the Iframe, and Cross-Domain Attribution

Why iframes hide conversions from GTM, the three patterns that solve it, working code for listening to postMessage events and for sending events from inside an iframe to the parent, and how to keep Google Ads attribution across the boundary.

Atiqul Islam
Written by Atiqul Islam

Tracking & analytics specialist. 278+ GTM, GA4 and server-side tracking projects for clients in the US, UK and Australia.

Rather not do this yourself? I set this up for clients every week. Free 15-minute audit, no obligation.

Book a Free Audit

Booking widgets, embedded forms, payment pages, chat tools, calculators and maps all arrive as iframes, and every one of them hides its conversion from the GTM container on your page. This guide explains the mechanics once, gives the three patterns that solve it, and provides the code for each so you can apply it to any embed, including ones no tool guide covers.

Why Iframes Block Tracking

An iframe is a separate document with its own window. When it is served from another domain, the browser enforces the same-origin policy: your page cannot read its contents, and it cannot read yours. GTM on your page therefore sees the iframe only as a box. Clicks, form fields and URL changes inside it are invisible.

Three things cross the boundary legitimately: messages sent with postMessage, a full redirect of the top window, and anything you install inside the iframe itself.

Pattern 1: Listen for postMessage

Most hosted tools send a message to the parent page when something meaningful happens. The parent listens, checks the sender's origin, and pushes to the dataLayer.

First, discover what the tool sends

Open the page with the embed, open the browser console, and run:

window.addEventListener('message', function (e) { console.log(e.origin, e.data); });

Complete a submission or booking in the iframe. Note the origin and the shape of the message for the completion step. It may be a string, a JSON string or an object.

Then, the listener tag

Create a Custom HTML tag on All Pages or on pages with the embed, and adapt the origin and match condition:

<script>
(function () {
  var ALLOWED_ORIGIN = 'vendor.com';
  window.addEventListener('message', function (e) {
    if (!e.origin || e.origin.indexOf(ALLOWED_ORIGIN) === -1) { return; }
    var data = e.data;
    if (typeof data === 'string') {
      try { data = JSON.parse(data); } catch (err) { data = { raw: data }; }
    }
    var isComplete =
      (data.event && /complete|submitted|scheduled/i.test(data.event)) ||
      (data.type && /submit|complete/i.test(data.type)) ||
      (data.raw && /complete|submitted/i.test(data.raw));
    if (!isComplete) { return; }

    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      event: 'iframe_conversion',
      iframe_vendor: ALLOWED_ORIGIN,
      iframe_event: data.event || data.type || data.raw || 'complete'
    });
  });
})();
</script>

Replace the loose regex with the exact string once you know it. The origin check prevents other iframes or malicious pages from triggering your conversion. Then build GA4, Google Ads and Meta tags on a Custom Event trigger for iframe_conversion.

The Calendly, Acuity, JotForm, Typeform, Tally and HubSpot guides on this site are all this pattern with the vendor's specific message filled in.

Pattern 2: GTM Inside the Iframe

If you control the iframe page, or the vendor lets you add scripts to it, install GTM inside and track normally, then bridge to the parent so one container sees everything.

Iframe to parent bridge

Inside the iframe page:

<script>
// Called on the iframe's own success event
function notifyParent(payload) {
  if (window.parent && window.parent !== window) {
    window.parent.postMessage({ event: 'child_form_success', payload: payload }, '*');
  }
}
notifyParent({ form_name: 'Quote request', user_email: 'name@example.com' });
</script>

Replace '*' with your parent page's origin in production so the message cannot be read by other pages that embed your iframe.

On the parent, the listener from Pattern 1 catches child_form_success and pushes it to the dataLayer. If the iframe and parent are on the same domain, skip postMessage and push directly:

if (window.parent && window.parent.dataLayer) {
  window.parent.dataLayer.push({ event: 'form_submit_success', form_name: 'Quote request' });
}

Cross-domain attribution

When conversions fire from GTM inside a third-party iframe, the click identifier from your page is not there. In the Conversion Linker on both containers, enable linking across domains and list both domains. In GA4's cross-domain settings, list both. Test by arriving with a test gclid and checking that the linker parameter appears in the iframe URL.

Pattern 3: Redirect to Your Page

Many tools let you set a redirect after completion. If they redirect the top window to a thank-you URL on your domain, track that page view. If they only redirect inside the iframe, the parent sees nothing; combine with Pattern 1 or set the redirect target to a page on your domain that itself pushes to the parent with the bridge from Pattern 2.

Engagement Signals as a Fallback

When none of the above is possible, detect interaction with the iframe so you at least know the visitor engaged:

<script>
(function () {
  var fired = false;
  window.addEventListener('blur', function () {
    setTimeout(function () {
      var el = document.activeElement;
      if (!fired && el && el.tagName === 'IFRAME') {
        fired = true;
        window.dataLayer.push({ event: 'iframe_focus', iframe_src: el.src || '' });
      }
    }, 0);
  });
})();
</script>

Treat iframe_focus as a secondary engagement event, never a conversion.

Security and Privacy Notes

Testing

  1. Console-log messages to confirm the completion message and origin.
  2. GTM Preview: complete the action, confirm one dataLayer event and one fire of each downstream tag.
  3. Trigger a non-completion message such as a resize; nothing should fire.
  4. For cross-domain, verify the linker parameter and that the conversion attributes to the campaign in Google Ads after a day.

Outcome

Any embedded tool becomes trackable with one of three patterns, your downstream tags stay identical, and attribution survives the domain boundary. When a new vendor appears, the console-log step tells you which pattern applies in five minutes.

Frequently Asked Questions

Why can't GTM see what happens inside an iframe?

An iframe is a separate document. If it comes from another domain, the browser's same-origin policy blocks the parent page from reading anything inside it: no clicks, no form fields, no URL changes. Your GTM container runs in the parent and sees only the iframe's outer frame.

What is postMessage?

It is the browser's sanctioned way for two documents from different origins to talk. The iframe calls window.parent.postMessage with some data; the parent listens with a message event listener. Most hosted form and booking tools post a message when a submission completes, which is what you listen for.

What if the iframe is my own page on my own domain?

Then the same-origin policy does not apply and you have two easy options: install GTM inside the iframe page and track normally, or have the iframe page push to the parent's dataLayer directly through window.parent.dataLayer.

How do I keep Google Ads attribution when the conversion happens on another domain?

Enable cross-domain linking in the Conversion Linker with both domains listed, and in the GA4 cross-domain settings, so the click identifier and client ID pass across the boundary. Without it, the conversion shows no campaign.

Can I just track clicks on the iframe?

You cannot see clicks inside it. You can detect that the iframe gained focus, which tells you the visitor interacted but nothing about the outcome. Use it as a secondary engagement signal at most.

Want This Done Right the First Time?

Book a free 15-minute audit call. I'll review your setup, show you what is broken, and tell you exactly what it takes to fix it.

Chat Now