Emtiaz
ServicesCase StudiesTestimonialsAboutFAQBlogs
Advanced Tracking8 min read

Tracking WPForms Submissions with GTM

Emtiaz Hossain

Author Name

Emtiaz Hossain

Last Edit : Jul 19, 2026, 10:48:00 AM
Tracking WPForms Submissions with GTM

TABLE OF CONTENTS▼
Mode 1: AJAX Forms (No Reload)Mode 2: Non-AJAX FormsWiring It Into GTMKey Takeaways

Contents

0%
Mode 1: AJAX Forms (No Reload)Mode 2: Non-AJAX FormsWiring It Into GTMKey Takeaways

WPForms has two distinct submission modes that behave completely differently for tracking purposes: AJAX forms that submit without a page reload, and non-AJAX forms that either redirect or reload the page on submit. Using the wrong pattern for the wrong mode means tags that either never fire or fire on every failed attempt.

Mode 1: AJAX Forms (No Reload)

WPForms fires no page reload for AJAX-enabled forms, so tracking has to hook into the form's own submit event directly, on the client, at the moment of submission.

javascript
<script>
  var form = document.querySelector("#wpforms-form-5499")  // Replace with your form ID

  form.addEventListener('submit', function(event) {
    var formData = new FormData(form);
    var formDataObject = {};
    formData.forEach(function(value, key) {
      formDataObject[key] = value;
    });

    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      event: 'wpform_submission_v2',
      formData: formDataObject
    });
  });
</script>

This uses the native FormData API to grab every field on the form generically, no need to hardcode each field's selector individually the way the CF7 pattern does. That makes it faster to set up, but it also means the tag fires on the submit attempt itself, not on confirmed success, worth knowing if the form has server-side validation that can still reject a submission after this event fires client-side.

Mode 2: Non-AJAX Forms

Non-AJAX WPForms submissions post the page directly, which opens up a different, more precise problem: multiple forms on the same site sharing overlapping ID prefixes, or validation that needs to be checked before the tag counts the submission as real.

javascript
<script>
(function() {
    var forms = document.querySelectorAll('form[id^="wpforms-form-5530"]:not(.wpforms-ajax-form)');
    forms.forEach(function(form) {
        var formId = form.getAttribute('data-formid');

        form.addEventListener('submit', function(event) {
            event.preventDefault();

            var form = this;
            var formData = new FormData(this);
            var wpFormData = {};

            var errorRequired = false;

            formData.forEach(function (value, key) {
                if(key) {
                    var inputField = form.querySelector('[name="'+key+'"]');

                    if(inputField) {
                        var isRequiredField = inputField.classList.contains('wpforms-field-required');

                        if(isRequiredField) {
                            if(inputField.tagName === 'SELECT') {
                                var selectedOptionVal = inputField.options[inputField.selectedIndex].text;
                                if(!selectedOptionVal) {
                                    errorRequired = true;
                                }
                            } else if((inputField.getAttribute('type') === 'email') && (!value || !value.includes('@'))) {
                                 errorRequired = true;
                            }
                            else if(!value) {
                                errorRequired = true;
                            }
                        }
                    }

                    var formattedKey = key.replace(/\[|\]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '');
                    wpFormData[formattedKey] = value;
                }
            });

            var requiredFieldSets = form.querySelectorAll('ul.wpforms-field-required');
            requiredFieldSets.forEach(function(fieldSet){
               if(fieldSet.querySelector('input[type="radio"]') || fieldSet.querySelector('input[type="checkbox"]')) {
                  if(!fieldSet.querySelector('input:checked')) {
                    errorRequired = true;
                  }
               }
            });

            if(!errorRequired) {
                window.dataLayer = window.dataLayer || [];
                delete wpFormData['wpforms_nonce'];
                dataLayer.push({event: 'wpform_submission_v2', formId: formId, inputs: wpFormData});
            }
        });
    });
})();
</script>

Three things this version does that the AJAX version doesn't need to:

Targets forms by ID prefix, not an exact match. form[id^="wpforms-form-5530"] catches every form starting with that ID, useful when WPForms renders slight ID variations across embeds of the same form on multiple pages, while :not(.wpforms-ajax-form) explicitly excludes any AJAX-mode instance so the two patterns don't double-fire on the same form.

Runs its own required-field validation before pushing. Non-AJAX forms redirect on native submit, so there's no server response to check afterward the way the CF7 pattern polls for one. Instead, this walks every field marked wpforms-field-required (including radio and checkbox groups, which need their own check since FormData only captures checked values) and sets an errorRequired flag if anything required is missing or invalid, blocking the dataLayer push entirely if validation would have failed.

Sanitizes field keys before pushing. WPForms names fields with bracket notation like wpforms[fields][1], this strips brackets and collapses underscores into clean, GTM-variable-friendly key names, and explicitly deletes the CSRF nonce field so it never ends up in reporting data.

Wiring It Into GTM

  1. Identify which mode the target form uses (check the form's HTML for a wpforms-ajax-form class) and use the matching pattern.
  2. Add the appropriate script as a Custom HTML tag, swapping in the real form ID.
  3. Trigger on the page(s) the form appears on.
  4. Build a GA4 Event tag listening for wpform_submission_v2, reading fields from either formData (AJAX mode) or inputs (non-AJAX mode), the two patterns push slightly different object shapes.
  5. Test both a valid and an incomplete submission in GTM Preview to confirm the non-AJAX version correctly blocks invalid attempts.

Key Takeaways

  • ▸Check whether a WPForms instance is AJAX or non-AJAX before choosing a tracking pattern, they need fundamentally different approaches
  • ▸AJAX-mode forms fire on submit attempt, not confirmed success, since there's no page reload to hook a later check into
  • ▸Non-AJAX forms need their own client-side required-field validation before the dataLayer push, since there's no later opportunity to check a response
  • ▸Sanitize WPForms' bracketed field names before pushing to the dataLayer so they're usable as clean GTM variables
#GTM#CustomTracking

Discussed this on LinkedIn

Found this helpful?

Drop a comment on the LinkedIn post – I read every reply and love connecting with tracking folks.

View on LinkedIn

Not Sure If Your Tracking Is Costing You?

Book a free 30-minute call. I will review your current tracking setup, show you where conversions are leaking, and tell you exactly what I would fix.

More Blog Posts

View all
Tracking Ninja Forms Submissions with GTM
Advanced Tracking

Tracking Ninja Forms Submissions with GTM

Ninja Forms fires its own jQuery success event with structured field data attached, making it the simplest of the major form plugins to track accurately in GTM.

Tracking Contact Form 7 Submissions with GTM
Advanced Tracking

Tracking Contact Form 7 Submissions with GTM

How to track Contact Form 7 submissions accurately in GTM by confirming CF7's own success message instead of trusting a click event that fires even on failed validation.

The Complete Guide to Form Tracking with Google Tag Manager
Advanced Tracking

The Complete Guide to Form Tracking with Google Tag Manager

A universal dataLayer pattern for tracking any form (name, email, phone, date, country, message) in GTM, the foundation every plugin-specific guide in this series builds on.

Emtiaz
EmailLinkedInPrivacyTerms

© 2026 Emtiaz Hossain