Skip to content

Track HubSpot Forms Submissions

HubSpot embedded forms fire three lifecycle callbacks — only one signals a confirmed submission. This recipe wires it via the native hbspt.forms.create callback and via postMessage (for embeds you do not control), extracts portal/form/guid metadata, and flags the double-count pitfall.

Valid as of April 2026, HubSpot Forms v2 embed.

HubSpot’s form embed exposes three callbacks:

CallbackWhen it firesUse for conversions?
onFormReadyForm DOM renderedNo
onFormSubmitUser clicked submit — before server validationNo
onFormSubmittedHubSpot confirmed the submissionYes

If you cannot control the embed (for example, the form is inside an iframe from another subdomain), HubSpot also posts a window.postMessage with eventName: 'onFormSubmitted' that you can listen for from the parent page.

When you control the embed script:

dataLayer.push() form_submission

Fires inside HubSpot's confirmed-submission callback.

<script src="https://js.hsforms.net/forms/embed/v2.js"></script>
<script>
hbspt.forms.create({
portalId: '12345678',
formId: '87654321-dead-beef-cafe-abcdef012345',
region: 'eu1',
onFormSubmitted: function ($form, data) {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'form_submission',
form_vendor: 'hubspot',
hs_portal_id: '12345678',
hs_form_id: '87654321-dead-beef-cafe-abcdef012345',
hs_form_guid: (data && data.submissionValues && data.submissionValues.hs_context) || undefined
});
}
});
</script>
  1. Create a Custom Event trigger

    • Trigger type: Custom Event
    • Event name: form_submission
    • This fires: Some Custom EventsDLV - form_vendor equals hubspot
  2. Create Data Layer Variables

    • DLV - form_vendorform_vendor
    • DLV - hs_portal_idhs_portal_id
    • DLV - hs_form_idhs_form_id
    • DLV - hs_form_guidhs_form_guid
  3. Create a GA4 Event Tag

    • Event name: generate_lead
    • Parameters:
      • form_vendor{{DLV - form_vendor}}
      • hs_portal_id{{DLV - hs_portal_id}}
      • hs_form_id{{DLV - hs_form_id}}
      • hs_form_guid{{DLV - hs_form_guid}}
    • Trigger: the Custom Event trigger above
  4. Test in Preview mode

    Submit a HubSpot form. Confirm form_submission appears with correct portal and form IDs.

Tag Configuration

GA4 - generate_lead (HubSpot)

Type
Google Analytics: GA4 Event
Trigger
Custom Event - form_submission (hubspot)
Variables
DLV - form_vendorDLV - hs_portal_idDLV - hs_form_idDLV - hs_form_guid

HubSpot’s own Tracking Code (_hsq) already records form submissions in HubSpot Analytics. If you also fire a HubSpot Custom Behavioural Event from GTM pointing at the same portal for the same submission, you will double-count inside HubSpot.

If you need a HubSpot custom event (for example, to surface the submission in a HubSpot workflow), use a different event name that represents a downstream action (lead_qualified, meeting_booked) rather than echoing form_submission.

onFormSubmitted receives a data object containing submissionValues:

onFormSubmitted: function ($form, data) {
var values = data.submissionValues || {};
// values.email, values.firstname, values.lastname, values.company
}
  1. Open GTM Preview mode on a page with a HubSpot form.
  2. Submit the form with valid data. Confirm form_submission appears with form_vendor: hubspot and the correct hs_form_id.
  3. Verify Tags Fired shows the GA4 tag.
  4. In GA4 DebugView, confirm generate_lead arrives within 15 seconds.
  5. Test failure path: submit with an invalid email. HubSpot’s inline validation blocks submit — confirm onFormSubmit does not leak into the dataLayer (if you mistakenly wired it, you will see an event here).
  6. Cross-check in HubSpot: confirm the submission appears in the HubSpot form’s Submissions tab, and that your contact was created or updated.

Using onFormSubmit instead of onFormSubmitted. The past-tense version is the one you want. onFormSubmit fires on click before server confirmation.

HubSpot script races with GTM. hbspt may not be defined when GTM’s Custom HTML tag runs. Wrap hbspt.forms.create(...) in a poller, or load the embed script from the same tag:

(function waitForHbspt() {
if (!window.hbspt) return setTimeout(waitForHbspt, 100);
hbspt.forms.create({ /* ... */ });
})();

Region mismatch. HubSpot has regional hosts (na1, eu1). If region is wrong, the embed silently fails. Check your portal’s region in HubSpot settings.

Multiple forms with same formId. If the same HubSpot form is embedded twice on a page, both fire onFormSubmitted independently. Your dataLayer push fires per submission — no dedupe needed.

Form in HubSpot CMS page. If the parent page is itself a HubSpot CMS page, the form may not use the embed script at all — it renders inline. Use a different trigger: watch for the form’s success DOM (.hs-form-confirmation becoming visible) via MutationObserver.

GDPR fields reset the form on load. If your form has GDPR fields, HubSpot re-renders the form when consent is provided, which fires onFormReady twice. This does not affect onFormSubmitted.