“GA4 says we got 41 leads last month. Our inbox says 12.” That’s how most Contact Form 7 tracking jobs start for me. Somebody set up a Form Submission trigger in GTM, it fired on every click of the submit button, including the ones that failed validation, and the numbers have been fiction ever since.
The fix takes about ten minutes, and it’s the same one I’ve used on more WordPress sites than I can count.
Why GTM’s form trigger doesn’t work with CF7
Contact Form 7 submits with AJAX. The page never reloads and there’s no thank-you page, so GTM’s built-in Form Submission trigger either misses the submit entirely or fires on every attempt, including empty fields and spam blocks.
CF7 does give us something better: a JavaScript event called wpcf7mailsent. It only fires after the email has actually been sent. That’s the moment we want to count.
The listener
<script>
(function () {
if (window.__analyticsrush_cf7) return;
window.__analyticsrush_cf7 = true;
document.addEventListener('wpcf7mailsent', function (e) {
var d = e.detail || {};
var email = '';
(d.inputs || []).forEach(function (f) {
if (!email && /email/i.test(f.name)) email = f.value;
});
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'cf7_submit',
form_id: d.contactFormId,
form_unit: d.unitTag,
user_email: email
});
}, false);
})();
</script>
It pushes one clean event, cf7_submit, with the form’s ID so you can tell your contact form apart from your quote form.
Set it up in GTM (step by step)
- Tags → New → Custom HTML. Paste the listener. Name it
cHTML - AnalyticsRush - Contact Form 7. - Trigger: DOM Ready. If you know which pages have forms, limit it to those.
- Variables → New → Data Layer Variable: create
form_idand, if you use Enhanced Conversions,user_email. - Triggers → New → Custom Event: event name
cf7_submit. - Your GA4 event tag: event name
generate_lead, parameterform_id={{DLV - form_id}}, fired by thecf7_submittrigger. - Preview, fill in the form for real, and check that
cf7_submitappears once with the right form ID.
Or skip steps 1–4 and import the ready-made container from the AnalyticsRush GitHub repo.
Mistakes I see all the time
- Keeping the old Form Submission trigger live. Now you count every lead twice. Pause it.
- Testing with a fake email. Some CF7 setups block it, the mail fails, and the event correctly doesn’t fire. People then think the listener is broken.
- Sending
user_emailto GA4. Never send it as a GA4 parameter. It’s there for Google Ads Enhanced Conversions and Meta CAPI only. - Caching plugins combining scripts. If nothing fires, check that GTM itself loads on the page. Aggressive optimisation plugins sometimes delay it until after the visitor interacts.
What I usually see on audits
The classic pattern: lead numbers in GA4 drop by half the day the listener goes live, and the client panics. Then we compare with the inbox, and the new number matches almost exactly. The old number was never real; the old trigger was counting button clicks.
Go further
CF7 also fires events when something goes wrong: wpcf7invalid, wpcf7spam and wpcf7mailfailed. The AnalyticsRush library has a second listener for those, so you can see how many people try and fail, not just how many succeed. That’s often where the quick wins are.
Bottom line
Count wpcf7mailsent, not clicks. Your GA4 leads will finally match your inbox, and your ad platforms will optimise on real leads.
Want your leads tracked server-side too, so ad blockers don’t eat them? AnalyticsRush sets up server-side GTM on Stape. Book a tracking audit.