- An embedded Jotform breaks the GA4 session. The form loads inside an iframe on Jotform's own domain, so anything tracked in there starts a separate, unrelated session instead of continuing yours.
- The fix is a listener, not a tag inside the iframe. One Custom HTML tag in Google Tag Manager listens for Jotform's
postMessageon submission, pushes an event to your owndataLayer, and fires GA4 from your domain, inside the session that already exists. - Check your embed type first. Iframe and script embeds need this fix. Full source embed does not, because there is no iframe to break the session in the first place.
- Capturing gclid and firing a conversion are two different jobs. A hidden field gets the click ID onto the lead record. The GTM fix tells Google Ads a session converted. You need both.
- Form fills are not revenue. Offline conversion import, keyed on the same gclid, is what stops Google Ads from optimising toward cheap, worthless submissions.
Someone on the Jotform subreddit spent three weeks trying to get Google Ads conversion tracking working on an embedded form. By the time a working answer arrived, he had already told his client to rebuild everything in Wix.
The fix was one Custom HTML tag in Google Tag Manager. About ten minutes of work. Credit where it is due: the working approach below came from Jotform's own support team in that thread, on their second attempt at the question.
That gap between the problem and the answer is worth writing about, because the reason it took three weeks is not incompetence. It is that this gets described as a Jotform problem when it is actually a browser problem, and the first answer everyone reaches for solves a different question than the one being asked.
First, work out which embed you have
This determines whether any of the rest applies to you. Jotform offers three, and they behave differently:
Iframe embed. The form loads in a separate document from Jotform's domain. The session boundary problem applies. The fix below is what you need.
Script embed. Renders an iframe underneath, so it behaves the same as above for these purposes. Same fix.
Full source embed. The form markup is inlined directly into your page with no iframe. There is no domain boundary, the GA4 session was never interrupted, and no postMessage is sent because there is no child document to send one. If this is your setup, the fix below will not fire and you do not need it. Track the submission the way you would any native form on your own site.
Check before you build anything. Following the wrong path here is how people lose an afternoon.
Why the session breaks
When someone clicks your ad they land on your domain with a click ID in the URL, and GA4 assigns them a client ID and a session. That client ID is stored against your domain. It cannot be read from anywhere else.
An iframe is a separate document loaded from Jotform's domain. Anything that fires inside it belongs to a different browsing context. Put a GA4 tag in there and you have not extended the session, you have started a second, unrelated one, on a domain you do not control and cannot configure.
That is the whole problem. The submission is real, the ad click was real, and there is no line connecting them.
Two problems that get confused
Almost every thread about this collapses two separate jobs into one question, which is why the answers so often miss.
Job one is capturing the click ID as data. You want gclid stored on the lead record so you can use it later. Jotform handles this natively and it is genuinely easy: add a Short Text field, set its unique name to gclid, and because the embedded form sits on a page whose URL already carries the parameter, the field populates itself.
Job two is registering the conversion inside the GA4 session. You want Google Ads to know that this session, from this campaign, converted, so bidding has something to learn from. A field value on a form submission cannot do that. Job one does not solve job two, and no amount of field-mapping will make it.
Do both. They are complementary, and the last section of this post is about why job one turns out to matter more than it first appears.
The fix that works
Jotform posts a message to the parent page when a submission completes. You listen for it on your own domain, push it into the dataLayer, and fire GA4 from there. The event lands on your site, inside the session that already exists, with the correct client ID attached. Nothing needs to run inside the iframe.
-
Add a Custom HTML tag in GTM, firing on All Pages, or scoped to the pages carrying the form:
<script> (function () { var ALLOWED = [ 'https://form.jotform.com', 'https://www.jotform.com', 'https://submit.jotform.com' ]; window.addEventListener('message', function (e) { if (ALLOWED.indexOf(e.origin) === -1) return; var payload = e.data; var completed = false; if (typeof payload === 'string') { completed = payload.indexOf('submission-completed') !== -1; } else if (payload && payload.action === 'submission-completed') { completed = true; } if (!completed) return; window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: 'jotform_submitted' }); }, false); })(); </script>Two notes on the snippet. The origin allowlist is an exact match, not a substring check. A substring check for the word “jotform” will happily accept a message from a domain someone registered specifically to send you one, and firing conversions on unverified input is how tracking data gets poisoned. Confirm the origins your embed actually uses in the browser console and adjust the array to match.
The payload is handled as both string and object because the message format has varied across embed types and versions. Log
e.dataonce on your own form and see which branch you land in, rather than trusting either. -
Create a Custom Event trigger with the event name
jotform_submitted. -
Attach your GA4 event tag to that trigger. Use
generate_leadfor the standard recommended event. -
Mark it as a key event in GA4 and import it as a conversion action in Google Ads, or fire your Google Ads conversion tag on the same trigger. Either works. Importing from GA4 keeps one source of truth.
-
Test in this order. GTM Preview to confirm the trigger fires on submission. Then GA4 DebugView to confirm the event actually arrives, because a tag showing as fired in Preview and a tag sending a hit are not the same thing. Then confirm the event carries the same session ID as the page view that started the visit. If the session IDs differ, the event is firing somewhere other than where you think it is, and you are back where you started.
Running more than one form on the site? Do not reach for a form ID in the payload without checking it is there first, or you will build segmentation on a value that is always null. The reliable approach is to scope the trigger by page path, since you know which form is on which page.
Three setups that look right and fail
GTM and GA4 installed inside the iframe. The intuitive move, and it produces the most misleading symptom: the tag reports as fired in GTM Preview, then GA4 receives nothing, often with a message about no hits being sent because other tags on the page may have taken action. You have two container instances running in two documents and they are competing. Even if a hit did land, it would carry a client ID scoped to a domain that is not yours.
Sending the submission to GA4 through Zapier or the Measurement Protocol. The event arrives. It arrives without the client ID and session ID that tie it to the ad click, so GA4 files it as an unattributed session of its own, and there is no recombining them afterwards. Worth understanding clearly, because plenty of people set this up, see events appearing in GA4, and reasonably conclude the job is done.
Redirecting to a thank-you page on your own domain. The logic is sound and the session does survive, which is why the person in that thread eventually shipped it. Two problems. Chained redirects are increasingly unreliable in modern browsers. And it only measures people who reach the confirmation page, so if anything interrupts that redirect the lead is sitting in your inbox while Google Ads records nothing. A reasonable fallback. Not the first choice when the form is embedded on your own site.
If your form is hosted on Jotform rather than embedded
The postMessage approach needs a parent page to listen on, so it does not apply. Your options narrow to sending people to a confirmation page on your own domain and tracking arrival there, with the caveats above, or moving the form onto your site so the submission happens where your tags already live.
If you are running paid traffic, moving it is usually worth the afternoon. Everything downstream of measurement gets easier once the conversion event and the ad click share a domain.
Check it again in a month
This setup breaks quietly, and a conversion count that stops climbing looks identical to a slow week.
Put a recurring reminder somewhere and do three things when it fires. Submit a test lead and watch it appear in GA4 DebugView. Check the Google Ads conversion action shows a recent conversion date rather than a stale one. Confirm nobody has swapped the embed code out for a different type, which is the single most common cause and never gets mentioned to whoever set up the tracking.
Anything that lives only in a browser at the moment of submission needs checking on a schedule. That is not pessimism about GTM, it is just what client-side measurement is.
The bit almost nobody sets up
Get the above working and Google Ads knows a form was submitted. It does not know whether that submission was worth anything.
For a service business this is the whole game. A hundred form fills where three become customers, and a hundred where thirty do, look identical to a bidding algorithm optimising on form fills. It will happily spend more on the campaign producing the worthless ones if they are cheaper per submission, which they usually are.
Offline conversion import is how you fix that. The mechanism:
gclidis stored on the lead record at submission, which is job one from earlier in this post.- Weeks later, that lead becomes a customer at a known value.
- You upload the click ID, a conversion time and that value to Google Ads, against a conversion action configured to accept imported data.
- Bidding now optimises toward the campaigns producing revenue instead of the campaigns producing cheap form fills.
The click ID has a validity window, so a sales cycle measured in months needs checking against Google's current limit before you rely on this. Consent state also matters for what can be imported. Both are worth confirming rather than assuming.
What consent and the wider obligations ask of a form is set out in GDPR compliant forms.
The blocker is almost never the upload. It is step one. The click ID has to be sitting on a durable record you can find again weeks later, next to a lead you can actually identify, and that is exactly what the GA4 route cannot give you. GA4 reports on sessions in aggregate, and sending personally identifying data into it is prohibited by its terms, so there is no path from a conversion count back to the named person who converted. Your conversion numbers and your lead list live in two different places with no join between them. We wrote about that limitation in more detail here.
Solving the other half
The way through is to attach the source to the lead record rather than relying on the session to carry it.
That is what Lead Source does with Jotform. You add one webhook URL in your form's integration settings and every submission arrives with the source, the campaign, the click ID and the page journey that preceded it attached to the lead itself. Nothing to add per form, no widget, no field-mapping, no payload to parse, and the click ID you need for the upload above is already on the record.
Two things change. The attribution sits on a record you own, so a named lead from four months ago still tells you which campaign and which landing page produced them, and you can put that next to what they turned out to be worth. And it does not depend on a listener firing at the right millisecond in someone's browser, so it does not quietly stop when a container gets edited, and it survives if you move off Jotform later.
None of that replaces the GTM setup above. Google Ads still needs a conversion signal and that fix is how you give it one. This answers the question the session-based route structurally cannot: not how many conversions, but which of these people came from where, and which of them were worth having.
Set up Jotform lead tracking free
Which fix for which setup
A quick reference for the setups covered above: what each one gets right, where it fails, and what it takes to fix.
| Setup | What happens | Fix |
|---|---|---|
| GTM + GA4 tag inside the Jotform iframe | Fires in Preview, GA4 gets nothing. Client ID is scoped to Jotform's domain, not yours. | Move the listener to your own domain (below). |
| Submission sent to GA4 via Zapier or the Measurement Protocol | Event arrives, but with no client ID or session ID, so GA4 logs it as an unattributed session. | Same postMessage listener, fired from your domain's existing session. |
| Redirect to a thank-you page on your own domain | Session survives, but chained redirects are unreliable and only converts people who reach the page. | Works as a fallback; the postMessage listener does not depend on the redirect completing. |
| postMessage listener in GTM on your own domain | Event fires inside the session that already exists, with the correct client ID attached. | This is the fix. See “The fix that works” above. |
| Jotform Full Source embed | No iframe, so the session was never broken and no postMessage is sent. | None needed. Track the submission like any native form. |