A lead-gen client was quietly losing track of half their conversions. They ran their whole business on Elementor Pro forms — contact, quote requests, demo bookings — and none of it was showing up reliably in GA4 or feeding their ad platforms. Their developer had tried GTM’s built-in Form Submission trigger, watched it fire inconsistently, and given up. The forms worked fine for the business; they were just invisible to the tracking.
This is one of the most common WordPress tracking headaches I get, and it has a clean fix once you understand why Elementor forms are hard to catch.
Why Elementor Pro forms slip past GTM
Elementor Pro doesn’t submit forms the old-fashioned way. There’s no page navigation and no thank-you page to catch a pageview on. Instead, the form is sent quietly in the background as an AJAX request to WordPress’s admin-ajax.php endpoint, and the page updates in place with a success message.
That breaks the usual tracking approaches:
- GTM’s native Form Submission trigger relies on the browser’s form submit event and a navigation. Elementor’s AJAX submit doesn’t produce a reliable one, so the trigger fires inconsistently or not at all.
- The “redirect to a thank-you page” workaround does work, but it means changing the form’s behavior — adding a redirect the client may not want, hurting the on-page UX, and coupling your tracking to a page that’s easy to break later.
What you actually want is to listen for the successful AJAX submission itself and push it into the data layer, without changing anything about how the form behaves. That’s a well-established technique in the GTM community, and here’s the version I use.
The listener
This script watches outgoing admin-ajax.php requests, waits for a successful Elementor Pro form response, and pushes a clean event into the data layer:
/**
* Elementor Pro form → dataLayer listener
* AnalyticsRush.com — Tuhin (Md Sarwar Hossain)
* Captures successful Elementor Pro submissions (admin-ajax) and pushes them to the dataLayer.
*/
(function () {
// Don't patch twice if this tag ever fires more than once
if (window.__arElementorHook) return;
window.__arElementorHook = true;
var proto = XMLHttpRequest.prototype;
var origOpen = proto.open;
var origSend = proto.send;
// Remember each request's URL so we can check it on send/load
proto.open = function (method, url) {
this.__arUrl = url;
return origOpen.apply(this, arguments);
};
proto.send = function (body) {
var xhr = this;
// Only interested in WordPress admin-ajax requests
if (xhr.__arUrl && /\/admin-ajax\.php/.test(xhr.__arUrl)) {
xhr.addEventListener('load', function () {
if (xhr.status !== 200) return;
// Response must be valid JSON and a successful submit
var res;
try { res = JSON.parse(xhr.responseText); } catch (e) { return; }
if (!res || res.success !== true) return;
// We only handle multipart FormData submissions
if (typeof FormData === 'undefined' || !(body instanceof FormData)) return;
var fields = {};
body.forEach(function (value, key) { fields[key] = value; });
// Make sure this is actually an Elementor Pro form send
if (fields.action !== 'elementor_pro_forms_send_form') return;
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'elementor_form_submit',
elementor_form: {
form_id: fields.form_id,
form_name: fields.form_name,
fields: fields
}
});
});
}
return origSend.apply(this, arguments);
};
})();
A few things worth understanding about how it works:
- It patches the XHR prototype once. Rather than replacing the global
XMLHttpRequest, it wrapsopenandsendon the prototype and remembers each request’s URL. The__arElementorHookguard means that even if the tag somehow fires twice, you won’t double-patch and double-push. - It only acts on the right requests. It filters to
admin-ajax.php, then only continues if the response is valid JSON,successis true, the body isFormData, and the action is Elementor’s form-send action. Every one of those checks is there to stop it firing on unrelated AJAX (WordPress usesadmin-ajaxfor lots of things). - It pushes a structured payload. You get the
form_id,form_name, and the fullfieldsobject, so you can build data layer variables for whatever you need downstream.
The GTM setup
- Custom HTML tag — paste the listener into a Custom HTML tag and fire it on All Pages, ideally early (Consent Initialization or Page View), so the patch is in place before anyone can submit a form.
- Custom Event trigger — event name
elementor_form_submit. If you have multiple forms and want to handle them differently, narrow it with a condition on the form ID. - Data Layer Variables — create DLVs for what you need, e.g.
elementor_form.form_id,elementor_form.form_name, or specific fields. - GA4 event tag — fire a clean event like
generate_leadorform_submiton that trigger, and mark it as a Key Event in GA4 so it shows up in your acquisition reports.
The trap that matters most: this payload contains PII
Here’s the part I never skip, because the fields object contains exactly what you’d expect from a form — names, email addresses, phone numbers. That’s personal data, and how you handle it from here matters more than the tracking itself.
Two rules I hold to:
- Don’t dump raw PII into GA4. Google’s terms prohibit sending personally identifiable information to Analytics, and it’s a real compliance exposure. Send the event and non-identifying details (form name, form ID) to GA4 — not the email and phone. If you need a value, count the lead, don’t ship the person’s contact details.
- For Meta CAPI or Google Ads Enhanced Conversions, hash first and respect consent. The email and phone captured here are gold for match quality — but they must be normalized and SHA-256 hashed before they leave, and only sent when the user has consented. This is where doing it server-side pays off: pass the captured fields into your server container, hash them there, and send clean, consent-gated conversions to the ad platforms. The form data becomes an enrichment source, handled properly, rather than raw PII leaking into places it shouldn’t.
Handling multiple forms
If a site has several Elementor forms, they all fire the same elementor_form_submit event. Use the form_id (and form_name where available) to tell them apart — a contact form and a newsletter signup usually deserve different GA4 events and different downstream handling. Inspect the actual fields object in Preview mode first, because Elementor’s exact field keys depend on how each form is built.
Mistakes to avoid
- Relying on GTM’s native Form Submission trigger. Elementor’s AJAX submit doesn’t reliably produce the event it needs. Listen for the AJAX response instead.
- Firing the listener too late. Load it on All Pages, early, so the XHR patch exists before any submission happens.
- Not filtering to successful submits. Push only when
successis true — otherwise you count failed and validation-error attempts as conversions. - Sending raw PII to GA4. Names, emails, and phones from the form must not go into Analytics. Keep GA4 to the event and non-identifying fields.
- Sending unhashed PII to ad platforms. Normalize and hash before it leaves the browser (better, server-side), and gate on consent.
- Ignoring the multi-form case. One event covers all forms — differentiate by
form_idor you’ll blur your conversions together.
Final thoughts
Elementor Pro forms aren’t untrackable — they just don’t announce themselves the way GTM expects. Listen for the successful admin-ajax submission, push a clean event into the data layer, and you capture every form without touching the form’s behavior or bolting on a redirect. Just remember what’s inside that payload: treat the PII with care, keep it out of GA4, and hash it before it feeds your ad platforms. Done right, this turns a whole category of “invisible” WordPress conversions into clean, attributable events.