Hubspot form tracking with (and w/o) Google Tag Manager

Table of Contents

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 HubSpotFormsV4 SDK) 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_step event 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 formsNew forms editor
How to recognize the embedhbspt.forms.create({...}) script<div class="hs-form-frame" data-form-id="..."> or hs-form-html div
Submission eventmessage event, type: 'hsFormCallback', eventName: 'onFormSubmitted'hs-form-event:on-submission:success
Step navigationNot availablehs-form-event:on-interaction:navigate (plus :next / :previous)
Where the values areevent.data.data.submissionValuesHubSpotFormsV4.getFormFromEvent(event).getFormFieldValues()
Field namesemail, firstname0-1/email, 0-1/firstname (object prefix)
onFormSubmit callback in the embedSupportedNot 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:

ParameterExampleNotes
eventform_submission / form_stepform_step fires only for new-editor multi-step forms
form_ida1b2c3d4-...The HubSpot form GUID, same format for both versions
form_versionlegacy / v4Handy 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_step fires in both directions. It runs when a visitor clicks Next and when they click Back. If you only want forward progress, listen to hs-form-event:on-interaction:navigate:next instead.
  • 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, so submission_data.email works for both form types.

Setting it up in Google Tag Manager

  1. 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.
  2. Create the Data Layer Variables. Variables → New → Data Layer Variable, one for each value you need:
    • dlv - form_id → form_id
    • dlv - form_version → form_version
    • dlv - form email → submission_data.email
    • dlv - form phone → submission_data.phone
    • dlv - form first name → submission_data.firstname
    • dlv - form last name → submission_data.lastname
  3. Create the triggers. Triggers → New → Custom Event. One with event name form_submission, one with form_step. To fire a tag for one form only, set the trigger to “Some Custom Events” with dlv - form_id equals that form’s GUID.
  4. Send the conversion to GA4. New GA4 Event tag, event name generate_lead (or your own), parameter form_id = {{dlv - form_id}}, fired on the form_submission trigger. Send the form ID only — never the submission data (see the privacy note below).
  5. 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_submission trigger. The same variables work for Meta’s Advanced Matching and LinkedIn’s Insight Tag conversions.
  6. Optional: track the funnel. Fire a GA4 event such as form_step on the form_step trigger, with form_id as 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 HubSpotFormsV4 in 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_data is empty. On legacy forms, some embed setups don’t send submissionValues. 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

  1. 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!

    1. 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?

      1. 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.

        1. 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.

          1. 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.

  2. 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

Leave a Reply

Your email address will not be published. Required fields are marked *

Related Posts