GoHighLevel agencies have a specific pain: the funnel is built in GHL, the forms and calendars are GHL embeds, and the client’s Google Ads and Meta campaigns have no idea any of it converted. I’ve built this tracking for GHL setups more times than any other platform on this list, and I’ve published a version of this listener before. This is the improved one.
Why GTM can’t see GHL forms
GoHighLevel (LeadConnector) forms and calendars are iframes. GTM on your site sees nothing that happens inside them.
The iframe does post messages to the parent page, but in an unusual format: an array, not an object. A form submission arrives with the lead data as a JSON string in position 2 of the array. A booking arrives as msgsndr-booking-complete in position 0.
The listener
<script>
(function () {
if (window.__analyticsrush_ghl) return;
window.__analyticsrush_ghl = true;
var lastLead = {};
window.addEventListener('message', function (e) {
var d = e.data;
if (Object.prototype.toString.call(d) !== '[object Array]') return;
window.dataLayer = window.dataLayer || [];
// Form submission: lead JSON in position 2
if (typeof d[2] === 'string' && d[2].indexOf('customer_id') > -1 && d[2].indexOf('email') > -1) {
try {
var lead = JSON.parse(d[2]);
lastLead = {
user_email: String(lead.email || '').trim().toLowerCase(),
user_phone: String(lead.phone || '').replace(/[^\d+]/g, ''),
lead_id: lead.customer_id || ''
};
window.dataLayer.push({ event: 'ghl_form_submit', lead_id: lastLead.lead_id, user_email: lastLead.user_email, user_phone: lastLead.user_phone });
} catch (err) {}
}
// Calendar booking completed
if (d[0] === 'msgsndr-booking-complete') {
window.dataLayer.push({ event: 'ghl_booking_complete', lead_id: lastLead.lead_id || '', user_email: lastLead.user_email || '', user_phone: lastLead.user_phone || '' });
}
});
})();
</script>
Unlike older versions you’ll find online, this one only pushes email, phone and the lead ID, not the whole lead record. That’s all you need for matching, and it keeps the rest of the visitor’s answers out of your data layer.
Set it up in GTM
- Custom HTML tag with the listener, on DOM Ready, on every page with a GHL form or calendar.
- Data Layer Variables:
lead_id,user_email,user_phone. - Two Custom Event triggers:
ghl_form_submitandghl_booking_complete. - GA4:
generate_leadon the form trigger,book_appointmenton the booking trigger. Don’t send email or phone to GA4. - Google Ads: conversion tags with Enhanced Conversions using
user_emailanduser_phone. - Meta: use
lead_idas the event ID, in both the browser pixel and your CAPI tag, so the two deduplicate. - Preview: submit the form, then complete a booking, and check both events.
Mistakes to avoid
- Tracking GHL’s own funnel pages with this. If the whole funnel is hosted on GHL, add GTM inside GHL’s tracking code settings instead. This listener is for GHL forms embedded on your own site.
- Calendar booked without a form first. If the visitor books directly, there’s no lead in memory, so the booking event won’t have email or phone. Use GHL webhooks into server-side GTM to fill that gap.
- GHL updates. GoHighLevel changes its embed now and then without notice. Re-test both events after any big GHL release.
What I usually see
The usual GHL setup I’m asked to fix: Meta Pixel installed inside GHL, nothing on the client’s main website, and conversions in Meta that are roughly a third of the leads in the GHL pipeline. After this listener plus CAPI with lead_id deduplication, Meta’s numbers sit much closer to the CRM, and the campaigns stabilise.
Bottom line
GHL messages are arrays, so generic listeners miss them. Read position 2 for leads, position 0 for bookings, and dedupe with the lead ID.
GHL agency? AnalyticsRush builds GHL webhook → server-side GTM → Meta CAPI and Google Ads pipelines on Stape. Book a call.