Form submissions are one of the most useful conversions to track on a WordPress site. A contact form, quote request, or newsletter signup tells you more about visitor intent than a page view ever will. But WPForms doesn’t push submission data into Google Tag Manager on its own, so you need a small listener that catches each submission and sends the details to your data layer.
This post walks through how that listener works, why WPForms needs two different approaches, and how to set up the matching trigger in GTM.
<script>
(function () {
'use strict';
window.dataLayer = window.dataLayer || [];
// Fields we never want to send to GTM
var IGNORED_FIELDS = ['action', 'wpforms_nonce', '_wp_http_referer'];
// Turn names like "wpforms[fields][3]" into "wpforms_fields_3"
function cleanKey(key) {
return key.replace(/\[|\]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '');
}
function formDataToObject(formData) {
var result = {};
formData.forEach(function (value, key) {
var name = cleanKey(key);
if (name && IGNORED_FIELDS.indexOf(name) === -1) {
result[name] = value;
}
});
return result;
}
function pushFormSubmit(formId, inputs) {
window.dataLayer.push({
event: 'wpform_submit',
formId: formId,
inputs: inputs
});
}
// Basic check of required fields before tracking a non-AJAX submit
function hasRequiredValues(form) {
var valid = true;
form.querySelectorAll('.wpforms-field-required').forEach(function (el) {
if (el.tagName === 'UL') {
// Checkbox / radio groups: at least one option must be checked
if (!el.querySelector('input:checked')) valid = false;
return;
}
if (el.tagName === 'SELECT') {
if (!el.value) valid = false;
return;
}
if (!el.value) {
valid = false;
} else if (el.type === 'email' && el.value.indexOf('@') === -1) {
valid = false;
}
});
return valid;
}
/* ---------- AJAX forms ----------
WPForms sends AJAX submissions to admin-ajax.php with action=wpforms_submit.
We patch XMLHttpRequest.prototype once, rather than replacing the constructor,
so other scripts on the page are not affected. */
var originalOpen = XMLHttpRequest.prototype.open;
var originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (method, url) {
this._wpformsUrl = String(url);
return originalOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function (body) {
var xhr = this;
var isWpformsSubmit =
/admin-ajax\.php/.test(this._wpformsUrl || '') &&
body instanceof FormData &&
body.get('action') === 'wpforms_submit';
if (isWpformsSubmit) {
var inputs = formDataToObject(body);
xhr.addEventListener('load', function () {
if (xhr.status !== 200) return;
try {
var response = JSON.parse(xhr.responseText);
// Only track submissions the server accepted
if (response && response.success) {
pushFormSubmit(inputs.wpforms_id, inputs);
}
} catch (e) {
// Non-JSON response: ignore
}
});
}
return originalSend.apply(this, arguments);
};
/* ---------- Non-AJAX forms ----------
Push the event on submit, but do NOT call preventDefault(),
so the form submits normally. */
document.addEventListener('submit', function (event) {
var form = event.target;
if (!(form instanceof HTMLFormElement)) return;
if (!/^wpforms-form-/.test(form.id)) return;
if (form.classList.contains('wpforms-ajax-form')) return; // handled above
if (!hasRequiredValues(form)) return;
var formId = form.getAttribute('data-formid');
pushFormSubmit(formId, formDataToObject(new FormData(form)));
}, true);
})();
</script>
Why WPForms Needs Two Approaches
WPForms can submit in two ways, and each one behaves differently in the browser.
The first is AJAX submission. The form sends its data in the background to WordPress’s admin-ajax.php endpoint, and the page stays where it is while a success message appears. Because nothing reloads, you can’t rely on a page load to detect the submission. Instead, the listener has to watch the network request itself and check the server’s response.
The second is standard submission. The browser sends the form and loads a new page. Here, the listener has to fire when the form is submitted, before the browser leaves the current page. The tracking code must not stop that submission from going through, or the visitor’s inquiry is lost.
A good listener handles both cases and keeps them separate.
What the Listener Does
The script has three main jobs.
It collects the form fields and cleans up their names. WPForms field names often look like wpforms[fields][3], which is awkward to use in GTM. Converting these to simpler keys makes the data easier to read in your reports and variables.
It removes fields you don’t want to send. Security tokens like the nonce and internal WordPress fields add noise, and you usually don’t want them stored in an analytics tool.
It pushes a wpform_submit event into the data layer with the form ID and the cleaned field values. GTM can then pick up that event and fire whatever tags you need, such as a Google Ads conversion or a GA4 event.
Only Count Successful Submissions
One common mistake is to record a conversion as soon as the visitor clicks submit. If the form has a validation error, or the server rejects the entry, you’ve counted a conversion that never happened. A better approach is to wait for the server to confirm the submission before pushing the event. For AJAX forms, that means checking the response for a success flag. For standard forms, the script performs a basic check that required fields are filled in before tracking, since the page is about to leave.
Set Up the Trigger in GTM
Once the script is on your site, GTM needs to know what to do with the event.
Create a Custom Event trigger and set its event name to wpform_submit. Attach it to the tags you want to fire, such as your conversion tag.
Next, create Data Layer Variables for the values you need. A variable for formId lets you tell different forms apart, which matters if your site has a contact form and a newsletter form. For individual fields, use variables that point to the inputs object, following the field names your listener produces.
Before you publish, open GTM Preview mode. Submit a test entry on both an AJAX form and a standard form, then confirm the event appears in the data layer and your tags fire once per successful submission. Testing both types catches most setup problems early.
Privacy Considerations
Form fields often contain personal information like names, email addresses, and phone numbers. Before sending any of it to a third-party tool, check your privacy policy and any consent requirements that apply to your visitors. A good rule is to send only the fields you actually need for reporting, and to avoid passing sensitive details at all.
Credit
This approach builds on the JetForm tracking method described by Md Hasanuzzamna at leomeasure.com. If you adapt the code from that article, link back to it and credit the author.


