Buildertrend Form Submit Listener
Buildertrend Form Submit Listener
Pushes a buildertrend_form_submit event to the dataLayer when a visitor
submits a Buildertrend Lead Capture Form embedded on your site, so you can
fire GA4, Google Ads, and Meta tags from a GTM Custom Event trigger.
How it works (and the trade-off)
The Buildertrend form lives in a cross-origin buildertrend.net iframe, so
GTM's built-in Form Submission trigger can't see it. The form doesn't send the
parent page a "submitted" message either. It only posts resize messages
({height: N}) so the host page can size the iframe.
The listener uses those resize messages:
- While the form is on screen,
Nis the form's height (hundreds of px). - When no form is rendered,
Nis Buildertrend's bare padding value,50. After the form has loaded, that only happens on the success screen ("Your information was successfully submitted.").
So once the full form has been seen, a later {height: 50} from the same
iframe counts as a successful submit. Validation errors and failed submits
leave the form on screen, so they don't fire.
The trade-off: this reads Buildertrend's undocumented resize behavior.
If they change it, the listener goes quiet until it's re-tuned (usually just
the EMPTY_HEIGHT value at the top of the script). Re-verify in GTM Preview
after Buildertrend updates.
Before you install: check the form's redirect setting
If the Lead Capture Form is set to redirect to a thank-you page after submit, Buildertrend sends the whole browser to that URL straight away and never shows its success screen, so this listener won't fire. In that case, skip the listener and use a Page View trigger on the thank-you page URL.
dataLayer output
| dataLayer event | dataLayer variables |
|---|---|
buildertrend_form_submit | bt_event_id, bt_builder_id, bt_form_origin |
bt_event_id: unique per submit. Use it as the Meta PixeleventIDfor CAPI deduplication.bt_builder_id: thebuilderIDfrom the iframe'ssrc(if found).bt_form_origin: the iframe origin that sent the message (for debugging).
No field values are available (no email, phone, or name). The iframe is cross-origin and Buildertrend doesn't send them to the parent page.
Installation
- GTM → Tags → New → Custom HTML.
- Paste the contents of
buildertrend-listener.html. - Triggering: All Pages (Page View). It only reacts to messages from
buildertrend.net, so it's safe to run everywhere. You can limit it to pages that have the form if you'd like. - Name it
cHTML - Buildertrend Form Listenerand save. - Create a Custom Event trigger with the Event Name
buildertrend_form_submit. - Create Data Layer Variables for any of the
bt_*keys you need (e.g.DLV - bt_event_id). - Attach your conversion tags (GA4
generate_lead, Google Ads conversion, Meta Lead witheventID={{DLV - bt_event_id}}) to the trigger from step 5.
Verify
- GTM Preview → submit a real test lead through the form.
- Confirm
buildertrend_form_submitappears once, after the "successfully submitted" message shows. - Delete the test lead in Buildertrend.
If nothing fires, open DevTools → Console and paste:
window.addEventListener('message', function (e) { console.log(e.origin, e.data); });
Then reload and submit again. You should see {height: …} messages from
https://buildertrend.net, ending in {height: 50} after the submit. If the
final value isn't 50, update EMPTY_HEIGHT in the script to match.
Known limitations
- Relies on undocumented behavior. A Buildertrend update can silently stop events.
- Redirect-after-submit forms aren't detected. Use a thank-you page trigger instead (see above).
- No field values. The payload has no email, phone, or name, so Meta CAPI matching is limited to what your other tags collect.
- Custom CSS edge case. If custom CSS in the form hides the whole form element, its height reads as zero and the listener could fire early. Standard styling is fine.
Disclaimer
Provided as-is under the MIT License. Test thoroughly before relying on it for revenue attribution.
Packaged by W.H. Boggs — whboggs.com
The code
<!--
Marketing Toolkit — Buildertrend Form Submit Listener
Version: v1.0.0
Last updated: 2026-09-23
https://github.com/whboggs/marketing-toolkit
-->
<script>
/*!
* Buildertrend Form Submit Listener
* Loaded from https://github.com/whboggs/marketing-toolkit
*
* Detects Buildertrend Lead Capture Form submissions (the cross-origin
* buildertrend.net iframe) and pushes a buildertrend_form_submit event to
* the GTM dataLayer.
*
* HOW IT WORKS: the Buildertrend form never tells the parent page "submitted".
* It only posts resize messages ({height: N}) so the host page can size the
* iframe. When the form is on screen, N is the form's height (hundreds of px).
* When no form is rendered, which after load happens only on the success
* "Your information was successfully submitted." screen, N is Buildertrend's
* bare padding value of 50. So: once we've seen the full form,
* a later {height: 50} from the same iframe = successful submit.
*
* WARNING: this reads Buildertrend's undocumented resize behavior, which they
* can change without notice. Re-verify in GTM Preview after Buildertrend
* updates.
*
* Created by whboggs — https://whboggs.com — Get in touch for a free tracking audit.
*
* MIT License — Copyright (c) 2026 W.H. Boggs
* https://github.com/whboggs/marketing-toolkit/blob/main/LICENSE
*/
(function () {
'use strict';
// dataLayer event name pushed on a successful submit
var DATALAYER_EVENT = 'buildertrend_form_submit';
// Height Buildertrend reports when no form is rendered (its bottom padding
// constant). Seen on initial load (before the form config arrives) and on
// the success screen.
var EMPTY_HEIGHT = 50;
// Per-iframe state, keyed by the iframe's window: has the full form been
// shown yet, and has this form already fired?
var frames = [];
// Only trust messages from buildertrend.net (or a subdomain of it)
function isBuildertrendOrigin(origin) {
return /^https:\/\/([a-z0-9-]+\.)*buildertrend\.net$/i.test(origin || '');
}
// Pull the height out of the object-shaped resize message. Buildertrend
// also posts an "h=123" string version, which is scaled by 1.25 and
// measured differently, so it's ignored here.
function readHeight(data) {
if (data && typeof data === 'object' && typeof data.height === 'number') {
return data.height;
}
return null;
}
// Find (or create) the tracked state for the iframe that sent a message
function getFrameState(source) {
for (var i = 0; i < frames.length; i++) {
if (frames[i].source === source) return frames[i];
}
var state = { source: source, formSeen: false, fired: false };
frames.push(state);
return state;
}
// Look up the builderID from the sending iframe's src, for reporting
function getBuilderId(source) {
var iframes = document.getElementsByTagName('iframe');
for (var i = 0; i < iframes.length; i++) {
if (iframes[i].contentWindow === source) {
var match = /[?&]builderID=([^&#]+)/i.exec(iframes[i].src || '');
return match ? decodeURIComponent(match[1]) : undefined;
}
}
return undefined;
}
// Unique ID for Meta Pixel + CAPI deduplication
function generateEventId() {
return String(new Date().getTime()) + '_' + Math.random().toString(36).substring(2, 15);
}
window.addEventListener('message', function (e) {
try {
// Ignore everything that isn't a Buildertrend resize message
if (!isBuildertrendOrigin(e.origin)) return;
var height = readHeight(e.data);
if (height === null) return;
var state = getFrameState(e.source);
if (height > EMPTY_HEIGHT) {
// Full form is on screen. Arm detection and allow a new fire in
// case the form is shown again (e.g. the iframe was reloaded).
state.formSeen = true;
state.fired = false;
return;
}
// Form disappeared after being shown = success screen
if (height === EMPTY_HEIGHT && state.formSeen && !state.fired) {
state.fired = true;
state.formSeen = false;
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: DATALAYER_EVENT,
bt_event_id: generateEventId(), // Meta Pixel + CAPI dedup key
bt_builder_id: getBuilderId(e.source),
bt_form_origin: e.origin
});
}
} catch (err) { /* never break the host page */ }
}, false);
})();
</script>