
Author Name
Emtiaz Hossain
Contact Form 7 doesn't give you a clean built-in dataLayer event, and its default AJAX submission means a simple click trigger on the submit button fires whether the submission actually succeeded or failed validation. This is the pattern used to track CF7 forms accurately: reading field values directly, then confirming success by watching for CF7's own success message rather than trusting the click event.
CF7 submits over AJAX by default. The submit button gets clicked whether the form passes validation or not, a required field left empty, an invalid email format, anything that blocks a real submission still lets the click event fire. A GTM tag built on a plain click trigger will report every attempt as a conversion, inflating form-submit numbers with people who never actually got through.
<script>
(function() {
var form = document.querySelector('form.wpcf7-form');
if (form) {
form.addEventListener('submit', function(e) {
if (window.hasFormSubmitted) return;
window.hasFormSubmitted = true;
var pagePath = window.location.pathname;
var userFromValue = pagePath === '/' ? 'homepage' : pagePath.replace(/\//g, '');
function getValue(selector) {
var element = document.querySelector(selector);
return element ? element.value : '';
}
var first_name = getValue("input[name='first-name']");
var last_name = getValue("input[name='last-name']");
var email = getValue("input[name='your-email']");
var phone = getValue("input[name='phonetext-519']");
var message = getValue("textarea");
function checkSuccessMessage() {
var responseOutput = document.querySelector('#wpcf7-f787-o1 > form > div.wpcf7-response-output');
if (responseOutput && responseOutput.textContent.trim() === 'Thank you for your message. It has been sent.') {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'cf7_submit_v2',
user_from: userFromValue,
formID: userFromValue + '_wpcf7-f787-o1',
user_data: {
first_name: first_name,
last_name: last_name,
email: email,
phone: phone,
message: message
}
});
clearInterval(checkInterval);
}
}
var checkInterval = setInterval(checkSuccessMessage, 500);
window.addEventListener('beforeunload', function() {
clearInterval(checkInterval);
});
});
}
})();
</script>Field values are captured at submit time, not after. CF7 sometimes clears or resets fields once a submission goes through, so reading first-name, your-email, and the rest inside the submit handler, before CF7's own AJAX response comes back, guarantees the values still reflect what the user actually typed.
Success is confirmed by CF7's own response text, not the submit event. CF7 renders its success or error message into a .wpcf7-response-output element after the AJAX call resolves. Polling for that element's exact success text via setInterval is what separates "the button was clicked" from "the message was actually sent." Adjust the selector (#wpcf7-f787-o1 in this example) and the exact success string to match the specific form being tracked, both are unique per CF7 instance and visible in the page's rendered HTML.
A submission flag prevents duplicate pushes. window.hasFormSubmitted blocks the handler from firing twice if a user double-clicks submit or the event listener somehow attaches more than once, a small guard that avoids inflating the count with duplicate events from the same real submission.
The interval is cleaned up on both success and page exit. clearInterval fires the moment the success message is detected, and again on beforeunload as a safety net, so the polling loop never keeps running in the background after the user has moved on.
wpcf7-f787-o1 identifiers to match the actual form (inspect the rendered page to find the real form ID and response-output container ID).cf7_submit_v2, mapping the user_data fields as needed for enhanced conversions or CAPI.Discussed this on LinkedIn
Found this helpful?
Drop a comment on the LinkedIn post – I read every reply and love connecting with tracking folks.
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.
WPForms has two submission modes, AJAX and non-AJAX, that need completely different GTM tracking patterns. Here's the real code for both, plus the validation logic non-AJAX forms need.
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.
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.