While the technical landscape is moving quickly to AI, Data Cloud and modernized systems for tracking web-attribution & engagement, many marketers rely on tried and true utm parameter and DOM values for capturing attribution metrics on their web forms.
While Hubspot has some very nice built in attribution functionality, Marketing Cloud Account Engagement (Pardot) leaves attribution up to the creative technical capacity of the teams managing the platform.
Our team has created the following solution, as one of the easiest to implement options for Pardot(MCAE) users:
Auto-Populating UTM & Tracking Fields in Pardot Forms (with Persistence + iFrame Support)
Capturing marketing attribution data in Pardot (Marketing Cloud Account Engagement) is essential for campaign reporting. But out of the box, Pardot forms don’t automatically collect UTM parameters, page context, or referrer information. To make matters worse, Pardot form field IDs are dynamic — making targeting them with JavaScript nearly impossible.
In this guide, we’ll show you how to add a lightweight script that:
- Pre-populates UTM parameters (
utm_source,utm_medium,utm_campaign,utm_content,utm_term) - Captures click IDs (
glcid,fbclid) - Adds Page Title, Page URL, Page URL (clean, no query strings)
- Logs the Referrer
- Works whether your form is hosted directly or embedded in an iframe
- Persists values across sessions with first-touch & last-touch storage
Why This Matters
- Attribution accuracy → Salesforce campaigns and Pardot reporting get cleaner data.
- Cross-visit tracking → A user may land with UTMs, navigate, and only fill the form later. Persistence ensures UTMs are still captured.
- Embedded forms → Most marketers embed Pardot forms via iframes. Our script handles both iframe + standalone cases.
Step 1: Add the Tracking Fields in Pardot
Create (or reuse) custom fields in Pardot with these exact labels (case-insensitive, underscores/dashes are okay):
utm_sourceutm_mediumutm_campaignutm_contentutm_termglcidfbclidpage-titlepage-urlpage-url-clean(new)referrer
👉 You can set them as hidden fields if you don’t want users to see them.

Step 2: Add the Script to Your Pardot Layout Template
Paste this into the Layout Template used by your Pardot forms (before </body>).
<script>
document.addEventListener("DOMContentLoaded", function () {
var TTL_DAYS = 90; // storage TTL
var ALLOWED_PARENTS = []; // e.g., ['https://www.yoursite.com']
function supportsLocalStorage(){try{var x="__ls";localStorage.setItem(x,x);localStorage.removeItem(x);return true;}catch(e){return false;}}
function setCookie(n,v,d){var t=new Date();t.setTime(t.getTime()+d*864e5);document.cookie=n+"="+encodeURIComponent(v)+"; expires="+t.toUTCString()+"; path=/; SameSite=Lax";}
function getCookie(n){var m=document.cookie.match(new RegExp("(^| )"+n+"=([^;]+)"));return m?decodeURIComponent(m[2]):null;}
var LS=supportsLocalStorage();
function storeSet(k,v){if(v==null)return;try{if(LS){localStorage.setItem(k,v);localStorage.setItem(k+"__ts",Date.now());}else{setCookie(k,v,TTL_DAYS);setCookie(k+"__ts",Date.now(),TTL_DAYS);}}catch(e){}}
function storeGet(k){try{var v=LS?localStorage.getItem(k):getCookie(k),ts=LS?localStorage.getItem(k+"__ts"):getCookie(k+"__ts");if(!v||!ts)return null;var a=Date.now()-Number(ts);if(isNaN(a)||a>TTL_DAYS*864e5){if(LS){localStorage.removeItem(k);localStorage.removeItem(k+"__ts");}else{setCookie(k,"",-1);setCookie(k+"__ts","",-1);}return null;}return v;}catch(e){return null;}}
function storeHas(k){return storeGet(k)!=null;}
function getAllParams(){var p=new URLSearchParams(location.search);if(location.hash&&location.hash.includes("=")){var h=new URLSearchParams(location.hash.replace(/^#/,""));h.forEach((v,k)=>{if(!p.has(k))p.set(k,v);});}return p;}
function norm(t){return(t||"").toLowerCase().replace(/[_\-:]/g," ").replace(/\s+/g," ").trim();}
function findInput(l){var id=l.getAttribute("for");if(id){var el=document.getElementById(id);if(el)return el;}var c=l.closest(".form-field")||l.parentElement;return c?c.querySelector("input,textarea,select"):null;}
function setIfEmpty(el,v){if(el&&v!=null&&v!==""){if(!(el.value||"").trim())el.value=v;}}
var ATTR=["utm_source","utm_medium","utm_campaign","utm_content","utm_term","glcid","fbclid"];
var params=getAllParams();
var parentOverrides={pageTitle:null,pageUrl:null,referrer:null,utms:{}};
var lt={};ATTR.forEach(k=>{var v=params.get(k);if(v)lt[k]=v;});
var refP=params.get("referrer")||params.get("ref");if(refP)lt.referrer=refP;else if(document.referrer)lt.referrer=document.referrer;
Object.keys(lt).forEach(k=>storeSet("lt_"+k,lt[k]));storeSet("lt_landing_ts",new Date().toISOString());
if(!storeHas("ft_landing_ts"))storeSet("ft_landing_ts",new Date().toISOString());
ATTR.forEach(k=>{if(lt[k]&&!storeHas("ft_"+k))storeSet("ft_"+k,lt[k]);});
if(!storeHas("ft_referrer")&<.referrer)storeSet("ft_referrer",lt.referrer);
function dynamicValues(){return{
utm_source:()=>params.get("utm_source")||parentOverrides.utms.utm_source||storeGet("lt_utm_source")||storeGet("ft_utm_source"),
utm_medium:()=>params.get("utm_medium")||parentOverrides.utms.utm_medium||storeGet("lt_utm_medium")||storeGet("ft_utm_medium"),
utm_campaign:()=>params.get("utm_campaign")||parentOverrides.utms.utm_campaign||storeGet("lt_utm_campaign")||storeGet("ft_utm_campaign"),
utm_content:()=>params.get("utm_content")||parentOverrides.utms.utm_content||storeGet("lt_utm_content")||storeGet("ft_utm_content"),
utm_term:()=>params.get("utm_term")||parentOverrides.utms.utm_term||storeGet("lt_utm_term")||storeGet("ft_utm_term"),
glcid:()=>params.get("glcid")||parentOverrides.utms.glcid||storeGet("lt_glcid")||storeGet("ft_glcid"),
fbclid:()=>params.get("fbclid")||parentOverrides.utms.fbclid||storeGet("lt_fbclid")||storeGet("ft_fbclid"),
"page-title":()=>parentOverrides.pageTitle||document.title||"",
"page-url":()=>parentOverrides.pageUrl||(window.self!==window.top?"":window.location.href)||"",
"page-url-clean":()=>{
var raw=parentOverrides.pageUrl||(window.self!==window.top?"":window.location.href)||"";
if(!raw)return "";
try{var u=new URL(raw,window.location.origin);return u.origin+u.pathname;}catch(e){return raw.split(/[?#]/)[0];}
},
referrer:()=>params.get("referrer")||params.get("ref")||parentOverrides.referrer||storeGet("lt_referrer")||storeGet("ft_referrer")||document.referrer||""
};}
function populate(){var dv=dynamicValues(),keys=Object.keys(dv),wanted=keys.map(norm),labels=document.querySelectorAll("label");
labels.forEach(l=>{var n=norm(l.textContent),i=wanted.indexOf(n);if(i!==-1){var k=keys[i],v=dv[k]();var el=findInput(l);setIfEmpty(el,v);}});}
populate(); // initial
window.addEventListener("message",function(ev){
var d=ev.data||{};if(!d||d.type!=="pardot-utms")return;
if(ALLOWED_PARENTS.length&&ALLOWED_PARENTS.indexOf(ev.origin)===-1)return;
var utms=d.utms||{};parentOverrides.utms={};ATTR.forEach(k=>{if(utms[k]){parentOverrides.utms[k]=utms[k];storeSet("lt_"+k,utms[k]);if(!storeHas("ft_"+k))storeSet("ft_"+k,utms[k]);}});
if(d.pageTitle)parentOverrides.pageTitle=d.pageTitle;if(d.pageUrl)parentOverrides.pageUrl=d.pageUrl;
if(d.referrer){parentOverrides.referrer=d.referrer;storeSet("lt_referrer",d.referrer);if(!storeHas("ft_referrer"))storeSet("ft_referrer",d.referrer);}
populate();
});
});
</script>
No-Cookie / No-Persistence Script
“What if You Don’t Want Persistence?”
Sometimes you don’t want to use cookies or localStorage — for example, if you want to minimize tracking, or your compliance team prefers session-only attribution.
In that case, you can use a no-persistence version of the script. This lightweight option:
- Only fills form fields based on the current page’s URL and the parent page (if embedded).
- Doesn’t write anything to storage, so values won’t carry across multiple visits.
- Perfect for simple attribution where you only care about the session in which the form is submitted.
Simply swap the full script in your Layout Template with the no-cookie version above.
👉 If you later decide you do want first-touch vs last-touch reporting, you can always upgrade back to the persistence-enabled version.
<script>
document.addEventListener("DOMContentLoaded", function () {
var ALLOWED_PARENTS = []; // optional: restrict origins
function getAllParams(){
var p=new URLSearchParams(location.search);
if(location.hash && location.hash.includes("=")){
var h=new URLSearchParams(location.hash.replace(/^#/,""));
h.forEach((v,k)=>{if(!p.has(k))p.set(k,v);});
}
return p;
}
function norm(t){return (t||"").toLowerCase().replace(/[_\-:]/g," ").replace(/\s+/g," ").trim();}
function findInput(l){var id=l.getAttribute("for");if(id){var el=document.getElementById(id);if(el)return el;}
var c=l.closest(".form-field")||l.parentElement;return c?c.querySelector("input,textarea,select"):null;}
function setIfEmpty(el,v){if(el&&v!=null&&v!==""){if(!(el.value||"").trim())el.value=v;}}
var params=getAllParams();
var parentOverrides={pageTitle:null,pageUrl:null,referrer:null,utms:{}};
function dynamicValues(){return{
utm_source:()=>params.get("utm_source")||parentOverrides.utms.utm_source,
utm_medium:()=>params.get("utm_medium")||parentOverrides.utms.utm_medium,
utm_campaign:()=>params.get("utm_campaign")||parentOverrides.utms.utm_campaign,
utm_content:()=>params.get("utm_content")||parentOverrides.utms.utm_content,
utm_term:()=>params.get("utm_term")||parentOverrides.utms.utm_term,
glcid:()=>params.get("glcid")||parentOverrides.utms.glcid,
fbclid:()=>params.get("fbclid")||parentOverrides.utms.fbclid,
"page-title":()=>parentOverrides.pageTitle||document.title||"",
"page-url":()=>parentOverrides.pageUrl||(window.self!==window.top?"":window.location.href)||"",
"page-url-clean":()=>{
var raw=parentOverrides.pageUrl||(window.self!==window.top?"":window.location.href)||"";
if(!raw)return "";
try{var u=new URL(raw,window.location.origin);return u.origin+u.pathname;}catch(e){return raw.split(/[?#]/)[0];}
},
referrer:()=>params.get("referrer")||params.get("ref")||parentOverrides.referrer||document.referrer||""
};}
function populate(){var dv=dynamicValues(),keys=Object.keys(dv),wanted=keys.map(norm),labels=document.querySelectorAll("label");
labels.forEach(l=>{var n=norm(l.textContent),i=wanted.indexOf(n);if(i!==-1){var k=keys[i],v=dv[k]();var el=findInput(l);setIfEmpty(el,v);}});}
populate();
window.addEventListener("message",function(ev){
var d=ev.data||{};if(d.type!=="pardot-utms")return;
if(ALLOWED_PARENTS.length&&ALLOWED_PARENTS.indexOf(ev.origin)===-1)return;
parentOverrides.utms=d.utms||{};
if(d.pageTitle)parentOverrides.pageTitle=d.pageTitle;
if(d.pageUrl)parentOverrides.pageUrl=d.pageUrl;
if(d.referrer)parentOverrides.referrer=d.referrer;
populate();
});
});
</script>
Step 3: Add the Parent Page Helper (For Embedded Forms)
If you’re embedding Pardot forms in another site, add this on the host page (after the iframe). It sends UTMs, title, URL, and referrer to the iframe.
<script>
(function(){
var iframe=document.querySelector('#pardotForm'); // adjust selector
if(!iframe)return;
function collectParams(){
var p=new URLSearchParams(location.search);
if(location.hash&&location.hash.indexOf('=')!==-1){var h=new URLSearchParams(location.hash.replace(/^#/,""));h.forEach((v,k)=>{if(!p.has(k))p.set(k,v);});}
var o={};p.forEach((v,k)=>o[k]=v);return o;
}
function payload(){return{type:'pardot-utms',utms:collectParams(),pageTitle:document.title||'',pageUrl:location.href||'',referrer:document.referrer||''};}
function send(){try{iframe.contentWindow.postMessage(payload(),'*');}catch(e){}}
iframe.addEventListener('load',send);setTimeout(send,800);
})();
</script>
No-Cookie / No-Persistence Script
Here’s the simplified Parent Page Helper that pairs with the no-cookie / no-persistence Layout Template script. It just collects UTMs (and hash UTMs), plus the parent page’s title/URL/referrer, and posts them to the Pardot iframe. No storage, no rewriting the iframe src.
Parent Page Helper (for iframes, no persistence)
<!-- Put this on the host page that embeds your Pardot form -->
<script>
(function(){
// Adjust this selector to match your iframe
var IFRAME_SELECTOR = '#pardotForm';
// Optional: lock to your Pardot origin (recommended). Otherwise '*' is used.
var TARGET_ORIGIN = '*'; // e.g., 'https://go.your-pardot-domain.com'
var iframe = document.querySelector(IFRAME_SELECTOR);
if (!iframe) return;
function collectParams(){
var p = new URLSearchParams(location.search);
// Also support hash params like #utm_source=...&utm_campaign=...
if (location.hash && location.hash.indexOf('=') !== -1) {
var h = new URLSearchParams(location.hash.replace(/^#/, ''));
h.forEach(function(v, k){ if (!p.has(k)) p.set(k, v); });
}
var out = {};
p.forEach(function(v, k){ out[k] = v; });
return out;
}
function buildPayload(){
return {
type: 'pardot-utms',
utms: collectParams(),
pageTitle: document.title || '',
pageUrl: window.location.href || '',
referrer: document.referrer || ''
};
}
function postToIframe(){
try { iframe.contentWindow.postMessage(buildPayload(), TARGET_ORIGIN); } catch(e){}
}
// Send once the iframe loads, plus a small delay to cover async rendering
iframe.addEventListener('load', postToIframe);
setTimeout(postToIframe, 800);
// Optional: for SPA sites — re-send after route changes/back/forward
['pushState','replaceState'].forEach(function(fn){
var orig = history[fn];
history[fn] = function(){
var ret = orig.apply(this, arguments);
setTimeout(postToIframe, 50);
return ret;
};
});
window.addEventListener('popstate', function(){ setTimeout(postToIframe, 50); });
})();
</script>
How it fits with your setup
- Use this helper on the embedding (parent) page.
- Use the no-cookie Layout Template script (the one you just got) inside your Pardot form’s Layout Template.
- The form will then prioritize:
- Parent page values (via
postMessage) when embedded - Current page values (URL/hash + DOM) when hosted directly
- Parent page values (via
- No values are stored beyond the current load.
Step 4: Test Your Setup
- Visit a test landing page with UTMs:
https://yoursite.com/landing?utm_source=google&utm_medium=cpc&utm_campaign=spring2025&glcid=abc123 - Open the form in dev tools → check hidden fields:
utm_source=googleutm_medium=cpcutm_campaign=spring2025glcid=abc123page-url= full parent URLpage-url-clean= same URL but without?utm_*referrer= previous page
Troubleshooting
- Fields empty → Ensure the label in Pardot matches exactly (
page-url-clean, notpage url clean). - Wrong URL in iframe → Confirm you added the Parent Page Helper.
- Referrer empty → Some browsers/sites restrict
document.referrer. You can still pass?referrer=myvaluein the URL. - Persistence not working → Check cookies/localStorage settings. Private browsing may block them.
Conclusion
With this setup, every Pardot form submission captures consistent attribution: UTMs, click IDs, page context, referrer, and now a clean page URL. The script works whether forms are standalone or embedded, and persistence ensures you don’t lose campaign data if the user navigates before converting.
🔥 Your Pardot forms are now iframe-safe, persistent, and attribution-ready.

