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
- Always check
e.originbefore acting on a message. - Never put the parent listener's result straight into a tag without validating the shape.
- Do not relay personal data through postMessage with a wildcard target origin.
- Consent applies inside the iframe as well; GTM installed inside needs the same consent setup as the parent.
Testing
- Console-log messages to confirm the completion message and origin.
- GTM Preview: complete the action, confirm one dataLayer event and one fire of each downstream tag.
- Trigger a non-completion message such as a resize; nothing should fire.
- 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.
