Standard embed
The Standard embed is one <script> tag. It runs on your page and writes a Jotform iframe in its place — so the fields end up on form.jotform.com, cross-origin, where the tag can't read or write them. What your page still controls is the URL that iframe loads, and Jotform prefills any field whose Unique Name matches a query parameter on it. The companion script below closes that gap. Your embed code doesn't change at all.
Add the companion script
Put it in the page's <head>, directly below the tag loader — or as its own GTM Custom HTML tag firing on all pages. Nothing to configure: it forwards every value on the tag's dataLayer event, so a field added to the field map later arrives here on its own. Drop anything you don't want on the submission into its EXCLUDE list — each one you drop frees room in the form URL for the rest.
<!-- MTK Attribution Jotform iframe Companion Script -->
<script>
(function () {
// Values to keep OUT. Both of these are long enough to crowd a URL past
// its ceiling on their own, and everything inside them already rides
// along in the individual fields. Add any field you don't want on the
// platform — each one you drop frees room for the rest.
var EXCLUDE = [
'mtk_journey_json',
'mtk_attribution_summary'
];
// Only when the platform's key has to differ from the field name.
var RENAME = {
// 'mtk_first_channel': 'first_channel'
};
// The tag also publishes each value under its retired spelling so older
// client forms keep filling. That is for forms, not for us — carrying both
// would double the payload for nothing.
var LEGACY_PREFIXES = ['mtk_ft_', 'mtk_lt_'];
var LEGACY_NAMES = [
'mtk_time_since_paid',
'mtk_conversion_timestamp',
'mtk_summary',
'mtk_touch_path',
'mtk_touch_path_json'
];
// Titles the generic rule can't produce (click IDs stay lowercase).
var TITLE_EXCEPTIONS = {"mtk_gclid":"gclid","mtk_gbraid":"gbraid","mtk_wbraid":"wbraid","mtk_msclkid":"msclkid","mtk_page_url":"Page URL","mtk_fbclid":"fbclid","mtk_fbc":"fbc","mtk_fbp":"fbp"};
function isLegacy(key) {
for (var i = 0; i < LEGACY_PREFIXES.length; i++) {
if (key.indexOf(LEGACY_PREFIXES[i]) === 0) return true;
}
return LEGACY_NAMES.indexOf(key) > -1;
}
// 'mtk_first_channel' -> 'First Channel', matching the Field Name the
// dashboard shows for that row.
function titleOf(key) {
if (TITLE_EXCEPTIONS[key]) return TITLE_EXCEPTIONS[key];
var words = key.replace(/^mtk_/, '').split('_');
var out = [];
for (var i = 0; i < words.length; i++) {
if (!words[i]) continue;
out.push(
words[i] === 'json'
? 'JSON'
: words[i].charAt(0).toUpperCase() + words[i].slice(1)
);
}
return out.join(' ');
}
// Order values are added in, which on a length-capped platform is also
// drop priority: what a lead is worth least without goes last. Anything
// not listed (a custom field, a new default) sorts after these, so a drop
// is predictable rather than whatever order the event happened to be in.
var PRIORITY = [
'mtk_last_channel', 'mtk_last_source', 'mtk_last_medium', 'mtk_last_campaign',
'mtk_gclid', 'mtk_gbraid', 'mtk_wbraid', 'mtk_msclkid',
'mtk_first_channel', 'mtk_first_source', 'mtk_first_medium', 'mtk_first_campaign',
'mtk_page_path', 'mtk_page_url',
'mtk_conversion_unix', 'mtk_conversion_datetime', 'mtk_custom_conversion_time',
'mtk_fbclid', 'mtk_fbc', 'mtk_fbp',
'mtk_last_landing_page', 'mtk_last_landing_page_group', 'mtk_first_landing_page',
'mtk_session_count', 'mtk_pageview_count', 'mtk_paid_touch_count',
'mtk_last_paid_touch', 'mtk_time_since_last_paid_touch', 'mtk_time_to_conversion',
'mtk_journey_string'
];
// Every mtk_* value the event carries, minus the excluded and the retired.
function keysFrom(data) {
var out = [];
for (var key in data) {
if (!data.hasOwnProperty(key)) continue;
if (key.indexOf('mtk_') !== 0) continue;
if (isLegacy(key)) continue;
if (EXCLUDE.indexOf(key) > -1) continue;
out.push(key);
}
out.sort(function (a, b) {
var ia = PRIORITY.indexOf(a), ib = PRIORITY.indexOf(b);
if (ia === -1 && ib === -1) return a < b ? -1 : a > b ? 1 : 0;
if (ia === -1) return 1;
if (ib === -1) return -1;
return ia - ib;
});
return out;
}
var EXTRA_HOSTS = [];
var HOST_RE = /(^|\.)(jotform\.(com|us|ca|eu|io|pro)|jotformeu\.com|jotfor\.ms)$/i;
var PATH_RE = /^\/(jsform\/)?\d{6,}(\/|$|\?)/;
var URL_ATTRS = ['src', 'data-src', 'data-lazy-src', 'data-litespeed-src'];
var MAX_WAIT_MS = 3000;
var MAX_URL_LEN = 1800;
var pending = [];
var flushed = false;
function attributionData() {
var dl = window.dataLayer || [];
for (var i = dl.length - 1; i >= 0; i--) {
if (dl[i] && dl[i].event === 'mtk_attribution') return dl[i];
}
return null;
}
function isFormUrl(url) {
var m = /^(?:https?:)?\/\/([^\/?#]+)([^?#]*)/.exec(url);
if (!m) return false;
var host = m[1].replace(/:\d+$/, '');
var path = m[2] || '/';
if (EXTRA_HOSTS.indexOf(host) > -1) return PATH_RE.test(path);
return HOST_RE.test(host) && PATH_RE.test(path);
}
function formUrl(el) {
if (!el || el.tagName !== 'IFRAME') return null;
for (var i = 0; i < URL_ATTRS.length; i++) {
var value = el.getAttribute(URL_ATTRS[i]);
if (value && isFormUrl(value)) return { attr: URL_ATTRS[i], url: value };
}
return null;
}
function withAttribution(url, data) {
url = url.replace('/jsform/', '/');
if (!data) return url;
var keys = keysFrom(data);
var sep = url.indexOf('?') > -1 ? '&' : '?';
var qs = '';
var dropped = [];
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var value = data[key];
if (value === undefined || value === null || value === '') continue;
var pair = encodeURIComponent(RENAME[key] || key) + '=' + encodeURIComponent(String(value));
if (url.length + sep.length + qs.length + pair.length + 1 > MAX_URL_LEN) {
dropped.push(key);
continue;
}
qs += (qs ? '&' : '') + pair;
}
if (dropped.length && window.console && console.warn) {
console.warn(
'[MTK Jotform] form URL hit its ' + MAX_URL_LEN + '-character ceiling; ' +
'these did not fit: ' + dropped.join(', ')
);
}
return qs ? url + sep + qs : url;
}
function done(el, attr, url) {
el.setAttribute('data-mtk-done', '1');
el.setAttribute(attr, withAttribution(url, attributionData()));
}
function resolve(el) {
var parkedUrl = el.getAttribute('data-mtk-src');
if (parkedUrl) return done(el, 'src', parkedUrl);
var found = formUrl(el);
if (found) done(el, found.attr, found.url);
}
function consider(el) {
if (!el || el.nodeType !== 1) return;
if (el.getAttribute && el.getAttribute('data-mtk-done')) return;
var found = formUrl(el);
if (!found) return;
if (found.attr !== 'src') {
if (flushed) resolve(el);
else if (pending.indexOf(el) === -1) pending.push(el);
return;
}
if (flushed) return resolve(el);
el.setAttribute('data-mtk-src', found.url);
el.removeAttribute('src');
if (pending.indexOf(el) === -1) pending.push(el);
}
function flush() {
if (flushed) return;
flushed = true;
for (var i = 0; i < pending.length; i++) resolve(pending[i]);
pending = [];
}
function scan(node) {
if (!node || node.nodeType !== 1) return;
consider(node);
if (!node.querySelectorAll) return;
var frames = node.querySelectorAll('iframe');
for (var i = 0; i < frames.length; i++) consider(frames[i]);
}
new MutationObserver(function (muts) {
for (var i = 0; i < muts.length; i++) {
var m = muts[i];
if (m.type === 'attributes') consider(m.target);
else for (var j = 0; j < m.addedNodes.length; j++) scan(m.addedNodes[j]);
}
}).observe(document.documentElement, {
childList: true, subtree: true,
attributes: true, attributeFilter: URL_ATTRS
});
scan(document.documentElement);
(function wait(start) {
if (attributionData()) return flush();
if (Date.now() - start > MAX_WAIT_MS) return flush();
setTimeout(function () { wait(start); }, 50);
})(Date.now());
})();
</script>
<!-- End MTK Attribution Jotform iframe Companion Script -->It watches for the form URL wherever it appears: assigned to src when the embed script inserts the frame, assigned a tick later, or parked in a lazy-load data-src. Load order is therefore not critical, which is why GTM works. Iframes that aren't Jotform forms are left alone.
Paste the embed code — unchanged
In the form builder, go to Publish → Embed, pick Embed (the standard option), and click Copy Code. Paste it on your page where the form should appear. It looks like this:
<script type="text/javascript" src="https://form.jotform.com/jsform/241076660384054"></script>
Leave it exactly as Jotform gave it to you — the companion works off the iframe the script creates, not off the script tag, so there is nothing to edit here. Don't add defer or move it into a lazy-loaded block; the embed script writes the frame where it sits.
Publish and verify
Publish, then load the page in a private window with a test URL like ?utm_source=googleads&utm_medium=cpc, submit the form, and check the entry under Submissions in Jotform.
To check it without submitting, run this in the console with the context set to top, not the form's frame:
document.querySelectorAll('iframe[data-mtk-done]').forEach(function (f) {
console.log(f.getAttribute('data-src') || f.getAttribute('src'));
});The companion stamps data-mtk-doneon every frame it tags, so a count of zero means it never recognized the iframe. Otherwise you'll see the URL it built, mtk_*parameters and all. Open that URL in its own tab — as a top-level page it's no longer cross-origin, so you can inspect the form's inputs and confirm the values arrive in the right fields.
1. Did the companion tag the frame? Zero
data-mtk-doneiframes means it never matched the URL. An Enterprise or white-label form domain won't match the built-in host list — add it to EXTRA_HOSTS at the top of the script.2. Are the parameters on the URL but the fields still empty? That's a Unique Name mismatch. Re-open the field in the builder and compare it character-for-character against the HTML Name; if Jotform rewrote it on save, put the mapping in
RENAME.3. Is the tag itself loading? No
mtk_attribution event in window.dataLayer means it was blocked, usually by an ad blocker — the companion then releases the form unchanged, which is why it still renders.Still stuck? Troubleshooting.
Journey JSON and Attribution Summary. Both are long enough to crowd the form URL past its practical ceiling on their own, so they ship in the companion's EXCLUDElist — everything they contain already arrives through the individual fields. The script skips any field that would push the URL too far rather than truncating it into half a value, and says which ones in the console when it does. If you see that warning, add the longest fields you don't need to EXCLUDE — that's the whole fix. Need those two? Use the Source Code embed instead.