This is the most common conversation I have in client kickoffs. The CMO is convinced a specific ad set is killing it. The CFO can't see the attribution and won't increase the budget. The agency in the middle is showing impressions and clicks, but no one can draw a clean line from that ad to this booked call to this signed contract.
The line exists. It got cut. Usually in the same place. Often in three places.
The five-link stitch
Every lead in your CRM should carry where it came from on its back. End to end, the chain is five hops:
If the chain is intact, you can look at a contact in your CRM and read back the original ad copy. If one link is missing, attribution collapses into "direct" or "organic" — the categories you can't optimize against.
Where the chain breaks
Three places. Almost always.
Break 1: Landing page strips the params
Marketer tags the ad URL with ?utm_source=fb-retargeting-warm&utm_campaign=q2-coaches. The landing page is a one-pager built with whatever template their site is on. The CTA button is hard-coded as <a href="/apply">. The user clicks. The query params evaporate. Done.
// What most templates ship
<a href="/apply" class="btn-primary">Apply now</a>
// What you actually need
<a href="/apply?source=fb-retargeting-warm-q2-coaches" class="btn-primary">Apply now</a>
You can't hand-write that for every page. The pattern is to read the inbound query params on page load and inject them into every internal CTA:
// One line on every landing page. Reads inbound utm_source/utm_campaign,
// appends to every internal anchor that points to a conversion endpoint.
function stitchSource() {
const inbound = new URLSearchParams(location.search);
const source = inbound.get('utm_source')
|| inbound.get('source')
|| inferFromReferrer(document.referrer);
if (!source) return;
document.querySelectorAll('a[href^="/apply"],a[href^="/book"]').forEach(a => {
const u = new URL(a.href, location.origin);
if (!u.searchParams.has('source')) u.searchParams.set('source', source);
a.href = u.toString();
});
}
stitchSource();
Now every internal CTA carries the ad's source forward. Even if the visitor hops two pages before they click.
Break 2: The form forgets the page it lives on
The CTA hands the visitor to /apply?source=fb-retargeting-warm-q2-coaches. They fill the form. The form posts { name, email, phone, company } to the lead endpoint. Source missing.
Every form on the site needs to read the URL params on page load and inject them as hidden fields the user can't see but the server gets:
// Inside the form, on page load — adds 4 hidden fields per submit
function attachProvenance(form) {
const url = new URL(location.href);
const out = {};
url.searchParams.forEach((v, k) => { out[k] = v; });
out.landing_page = location.pathname; // the page itself
out.referrer = document.referrer || 'direct'; // where they came from
out.submitted_at = new Date().toISOString(); // when
for (const [k, v] of Object.entries(out)) {
const input = document.createElement('input');
input.type = 'hidden'; input.name = k; input.value = v;
form.appendChild(input);
}
}
Now the lead payload that hits your endpoint carries the source, the landing page it converted on, the referrer, and a timestamp. Four fields. Worth it.
Break 3: The CRM stores the contact but not the tag
The endpoint receives { email, source: 'fb-retargeting-warm-q2-coaches', landing_page: '/apply', referrer: '...' }. It calls the CRM API. It says "create a contact named Adam Palmer with email [email protected]." The contact gets created. The source went into a note that nobody reads.
The CRM-level fix is to tag the contact with the source at create time, not attach it as freeform text:
// Tags drive workflows. Notes don't. Always create with tags.
async function upsertContact(payload) {
const tags = [
'as-website',
`form-${payload.form_type}`,
`src-${normalize(payload.source)}`, // ← THIS LINE
];
return await ghl.contacts.upsert({
email: payload.email,
firstName: payload.name?.split(' ')[0],
lastName: payload.name?.split(' ').slice(1).join(' '),
phone: payload.phone,
source: payload.source, // also store as native field
tags, // and tag-driven for workflows
});
}
function normalize(s) {
return String(s||'direct').replace(/[^a-z0-9-]/gi, '-').toLowerCase().slice(0, 64);
}
Now the contact has a tag like src-fb-retargeting-warm-q2-coaches. That tag can fire a workflow specifically for warm Facebook retargeting from the Q2 coaches campaign. The CRM has finally caught up to what the ad was.
The audit signal
You don't have to deploy the whole stitch to know whether yours is intact. Three minutes of click-tracing tells you.
- Open a private browser window. Pick the most expensive paid campaign you're running. Click your own ad.
- Click any internal link before you reach a form. Two pages forward.
- Open the form. View the HTML. Search for the value of your
utm_source. Is it there as a hidden field? - Submit a dummy lead. Open it in the CRM. Is there a tag containing the source?
If any of those four steps fails, your stitch is cut. The next time the CFO asks "is the Q2 coaches campaign working," your answer is going to be a shrug.
What you can do today
The stitch isn't a project. It's three patches.
- Patch your CTAs — drop the
stitchSource()snippet into a shared script. Loads on every page. Every internal link now carries the source forward. - Patch your forms —
attachProvenance()on form load. Every submission now carries source + landing page + referrer + timestamp. - Patch your CRM creation — tag at upsert. Workflows fire off tags. Notes don't.
None of these need a vendor. None need a budget. Total ship time is one afternoon for one developer. The result is that every contact in your CRM is readable as a story instead of an anonymous row.
At AutomateScale, every client's first month includes a lead-stitch audit. We click through every paid channel, every organic landing, every funnel — and trace the source all the way to the GHL tag. Then we patch the cuts. The result is usually 20-40% of historical leads recategorized from "direct" or "organic" into the actual campaign that produced them. Want the audit on your funnel? Apply for an audit.
The one-line summary
Attribution doesn't survive on its own. Five hops separate the ad from the booked call: ad, landing, CTA, form, CRM. The stitch is three lines of code at three of those hops. Without the stitch, you cannot optimize what you cannot trace. With it, every contact in your CRM is a readable story — and the next time someone asks which campaign is working, the answer is on the contact itself.