Update, November 2026: This post was originally published in June 2020 and has been fully rewritten. HubSpot’s new forms editor uses a new forms SDK that emits different browser events, so the old listener no longer catches every submission. The new script below tracks both legacy and new forms, passes the submitted values for Enhanced Conversions, and tracks the steps of multi-step forms. If you used the previous version of this post, swap your old Custom HTML tag for the new one.
HubSpot’s built-in reporting is fine for HubSpot, but you’ll usually want your form conversions in GA4, Google Ads, Meta and LinkedIn too. The easiest route is a thank-you page. Most HubSpot forms show an inline success message instead, though, so GTM’s built-in Form Submission trigger won’t help — the form lives in HubSpot’s own code (often an iframe) and doesn’t fire a standard submit.
The fix is a small listener that catches HubSpot’s own form events and pushes them to the dataLayer. From there you can fire any tag you like.
What’s new in this version:
- Support for the new forms editor (the
HubSpotFormsV4SDK) alongside legacy forms, in one script. - Submission data in the dataLayer — the field values, ready for Google Ads Enhanced Conversions and Meta Advanced Matching.
- Multi-step form tracking — a
form_stepevent each time a visitor moves between steps, so you can build a funnel and see where people drop off.
Legacy forms vs. new forms
HubSpot now has two form systems, and each talks to the page differently. Legacy forms send a postMessage called hsFormCallback. Forms built in the updated forms editor dispatch DOM events prefixed with hs-form-event: and expose a HubSpotFormsV4 object for reading the form’s values.
| Legacy forms | New forms editor | |
|---|---|---|
| How to recognize the embed | hbspt.forms.create({...}) script | <div class="hs-form-frame" data-form-id="..."> or hs-form-html div |
| Submission event | message event, type: 'hsFormCallback', eventName: 'onFormSubmitted' | hs-form-event:on-submission:success |
| Step navigation | Not available | hs-form-event:on-interaction:navigate (plus :next / :previous) |
| Where the values are | event.data.data.submissionValues | HubSpotFormsV4.getFormFromEvent(event).getFormFieldValues() |
| Field names | email, firstname | 0-1/email, 0-1/firstname (object prefix) |
onFormSubmit callback in the embed | Supported | Not supported |
If you’re not sure which you have, open the page, open the browser console and type HubSpotFormsV4. If it returns an object, the page runs a new-editor form. Most accounts have a mix of both for now, which is why the script handles both.
The listener script
One script covers all three cases: legacy submissions, new-editor submissions, and new-editor step changes. Every push uses the same event and parameter names, so one set of GTM triggers works for both form types.
<script type="text/javascript">
(function () {
window.dataLayer = window.dataLayer || [];
// Flatten the v4 field array into an object and drop the object prefix ("0-1/email" -> "email")
function toFormData(fields) {
var data = {};
for (var i = 0; i < fields.length; i++) {
var name = fields[i].name.replace(/^\d+-\d+\//, '');
data[name] = fields[i].value;
}
return data;
}
// Get the exact form instance that fired the event (works with several forms on a page)
function getV4Form(event) {
var sdk = window.HubSpotFormsV4 || window.HubspotFormsV4;
return sdk ? sdk.getFormFromEvent(event) : null;
}
function pushV4Event(eventName, event) {
try {
var form = getV4Form(event);
if (!form) {
console.error('HubSpot form not found for ' + eventName);
return;
}
form.getFormFieldValues().then(function (fields) {
window.dataLayer.push({
'event': eventName,
'form_id': event.detail.formId,
'form_version': 'v4',
'submission_data': toFormData(fields)
});
}).catch(function (error) {
console.error('Error fetching HubSpot form fields:', error);
});
} catch (error) {
console.error('HubSpot form tracking error:', error);
}
}
// 1. Legacy HubSpot forms - submission
window.addEventListener('message', function (event) {
if (event.data && event.data.type === 'hsFormCallback' && event.data.eventName === 'onFormSubmitted') {
window.dataLayer.push({
'event': 'form_submission',
'form_id': event.data.id,
'form_version': 'legacy',
'submission_data': (event.data.data && event.data.data.submissionValues) || {}
});
}
});
// 2. New forms editor - successful submission
window.addEventListener('hs-form-event:on-submission:success', function (event) {
pushV4Event('form_submission', event);
});
// 3. New forms editor - multi-step navigation
window.addEventListener('hs-form-event:on-interaction:navigate', function (event) {
pushV4Event('form_step', event);
});
})();
</script>
What each push contains:
| Parameter | Example | Notes |
|---|---|---|
event | form_submission / form_step | form_step fires only for new-editor multi-step forms |
form_id | a1b2c3d4-... | The HubSpot form GUID, same format for both versions |
form_version | legacy / v4 | Handy for debugging and for checking migration progress |
submission_data | {email: '[email protected]', firstname: 'Jane'} | The field values, keyed by HubSpot’s internal field name |
A few notes on the script:
form_stepfires in both directions. It runs when a visitor clicks Next and when they click Back. If you only want forward progress, listen tohs-form-event:on-interaction:navigate:nextinstead.- Step events carry the values entered so far. That lets you see which fields people fill in before they abandon a multi-step form.
- The prefix is stripped. New-editor field names include the CRM object (
0-1/for contacts,0-2/for companies). The script removes it, sosubmission_data.emailworks for both form types.
Setting it up in Google Tag Manager
- Create the listener tag. New tag → Custom HTML. Paste the script above and name it something like
cHTML - HubSpot Form Listener. Set the trigger to Initialization – All Pages (or All Pages), so the listener is in place before the form loads. - Create the Data Layer Variables. Variables → New → Data Layer Variable, one for each value you need:
dlv - form_id→form_iddlv - form_version→form_versiondlv - form email→submission_data.emaildlv - form phone→submission_data.phonedlv - form first name→submission_data.firstnamedlv - form last name→submission_data.lastname
- Create the triggers. Triggers → New → Custom Event. One with event name
form_submission, one withform_step. To fire a tag for one form only, set the trigger to “Some Custom Events” withdlv - form_idequals that form’s GUID. - Send the conversion to GA4. New GA4 Event tag, event name
generate_lead(or your own), parameterform_id={{dlv - form_id}}, fired on theform_submissiontrigger. Send the form ID only — never the submission data (see the privacy note below). - Set up Google Ads Enhanced Conversions. In your Google Ads Conversion Tracking tag, tick “Include user-provided data from your website” and create a new User-Provided Data variable. Choose Manual configuration and map Email, Phone, First name and Last name to the variables from step 2. Fire it on the
form_submissiontrigger. The same variables work for Meta’s Advanced Matching and LinkedIn’s Insight Tag conversions. - Optional: track the funnel. Fire a GA4 event such as
form_stepon theform_steptrigger, withform_idas a parameter. A Funnel Exploration in GA4 then shows where visitors drop off in a multi-step form.
Pro tip: for readable form names in your reports, create a Lookup Table variable with dlv - form_id as the input and map each GUID to a friendly name like “Demo request”. Send that as a parameter alongside the ID.
Tracking without Google Tag Manager
The same listener works without GTM. Add it to your site’s code (after your gtag and Meta Pixel base codes), and point the pushes to your tracking calls instead of the dataLayer.
The cleanest way is to keep the script as is and add a small function that sends each push to your platforms. Put this right after window.dataLayer = window.dataLayer || []; and replace every window.dataLayer.push( in the script with sendFormEvent(:
function sendFormEvent(payload) {
var data = payload.submission_data || {};
if (payload.event === 'form_submission') {
// Google Ads Enhanced Conversions - set user data before the conversion
gtag('set', 'user_data', {
'email': data.email,
'phone_number': data.phone
});
gtag('event', 'conversion', { 'send_to': 'AW-XXXXXXXXX/XXXXXXXXXXXX' });
// GA4 - form ID only, no personal data
gtag('event', 'generate_lead', { 'form_id': payload.form_id });
// Meta Pixel
fbq('track', 'Lead');
}
if (payload.event === 'form_step') {
gtag('event', 'form_step', { 'form_id': payload.form_id });
}
}
Replace AW-XXXXXXXXX/XXXXXXXXXXXX with your own conversion ID and label. Phone numbers should be in E.164 format (+972...) for the best match rate.
A note for readers of the old version: it showed an onFormSubmit callback inside hbspt.forms.create() to track one specific form. That only works with legacy forms. With the new editor there’s no callback, so filter on form_id in the listener instead.
Testing and troubleshooting
Open GTM Preview, submit a test form and look for form_submission in the event list. Click it and check the Data Layer tab: you should see form_id, form_version and submission_data with your test values. For a multi-step form, click Next and confirm a form_step event appears.
If nothing shows up:
- No event at all on a new-editor form. Type
HubSpotFormsV4in the console. If it’s undefined, the form hasn’t loaded yet or it’s a legacy form. Also check that the listener tag fires on Initialization or All Pages, not on a later trigger. - Event fires but
submission_datais empty. On legacy forms, some embed setups don’t sendsubmissionValues. Test on the page itself, not inside HubSpot’s preview. - Wrong form’s data. You’re probably on an older script that uses
getForms()[0]. Switch to the version above. - The success event fires twice. The listener tag is probably firing twice, for example on both All Pages and History Change. Keep one trigger.
A note on privacy
submission_data contains personal data. Use it only for tags built to hash and handle it, such as Google Ads Enhanced Conversions and Meta Advanced Matching. Don’t send it to GA4 — Google’s terms don’t allow personal data in Analytics. Make sure your consent setup covers these tags before they fire.
6 Responses
So I have a situation that someone like me who’s limited on coding knowledge has been trying to crack. My company has HubSpot as its marketing automation tool and I have an external form that runs on a SPA framework, tracked using GTM pushing semi-customized HubSpot tracking. We previously set up advanced e-commerce in GA to push a bunch of valuable non-PII data elements to the data layer which works correctly, but now I’m looking to automatically pull that data into the HubSpot contact record after the form submit. HubSpot natively recognizes some of the basic form elements and captures the form submission, but I haven’t been able to figure out how to pull in all of the other data elements already pushed to the data layer. Please help!
Hi Matt
It sound’s like you’re using Hubspot’s automated form listener to capture form submissions, which won’t let you select the data sent or it’s mapping.
Intuitively I would suggest populating the additional data as hidden fields in your form so they can be passed into HS.
It’s less complicated than it seems, it requires adding the hidden fields to the form and populating them with the data with a simple Javascript function.
On which CMS is your site built?
We use HubSpot for our main CMS but the form I’m referencing isn’t hosted on a regular CMS, rather it’s pulled together using bit bucket or something.
Also, through my research I saw that there are ways of pulling the data in on non-form submits using a measurement protocol? The idea of it sounds like a better option but understand it is probably more difficult.
I should add that for the hidden form field to work with this particular form, I would need to somehow push the new form field through GTM to capture on submission rather than updating the form code directly. Because of the sensitivity of the form and lack of availability, going through one of our developers isn’t an option right now.
The Measurement Protocol is a solution to push data to Google Analytics (essentially, GA’s API).
So to trigger this data to HS you would rather capture it on the client-side.
While it is technically possible to achieve this with GTM, I would strongly advise against this.
I’d take the long and safe route of properly adding these to the form as hidden fields.
Hi Elad Levy,
Nice Article it will help a lot to me for Implementing the tracking with HubSpot forms in GTM.
Keep writing these types of helpful articles Elad.
I having facing issues with tracking with HubSpot from in GTM. I Google it I found this site I implemented like this but I have tracked a few & a few are not tracked.
Now I am able to track perfectly
Thank you Very Much, Kudos to Elad Leavy, Great Support & help
Regards,
Ravindra