Skip to content

Practical B2B Attribution: UTMs and Self-Reported Data

By Shuk Huay Koh, Founder, Mir Tech

Published Updated 6 min read

Data HandlingIntermediateAttributionCRM
On this page

A B2B team that closes a handful of deals a quarter does not have an attribution-model problem. It has a capture problem. Cycles run for months, a buying committee has several people with their own browsers, and the touches that matter most, such as a recommendation in a private group or a podcast episode, leave no click behind.

So aim for answers that are good enough to guide a budget decision, and say plainly what they cannot show. The order we would build in for a founder-led team is clean tracking first, self-reported attribution second, multi-touch models last.

What each method can tell you

MethodAnswersBlind spots
UTMs and click IDsWhich campaign or link brought this visitAnything without a trackable click; dark social; offline
Self-reported ("How did you hear about us?")What made the buyer decide to get in touchMemory is fuzzy; people name the last thing they remember
CRM stage dataWhich sources turn into opportunities and revenueNeeds consistent fields and enough volume
Multi-touch modelsHow credit might be divided across tracked touchesCannot see untracked touches; credit rules are a choice, not a fact

The tracked view shows where people clicked. The self-reported view shows what they credit. The two often disagree, and that gap is the useful part.

Decide what the numbers are for

List the decisions the numbers should inform before you touch a tag: whether to keep paying for a newsletter sponsorship, whether LinkedIn ads or outbound produce better opportunities, whether a webinar series is worth the effort. If a question cannot change a decision, do not build reporting for it. For how fast deals move after they enter the pipeline, see pipeline velocity metrics.

UTM convention

UTM parameters are the base layer. Google's URL builder documentation explains how they feed campaign reports in Google Analytics. They only help if everyone uses them the same way, so put the convention in a shared sheet.

utm_source   = where the link lives        linkedin, newsletter-acme, google, partner-name
utm_medium   = type of placement           paid-social, cpc, email, referral, organic-social
utm_campaign = the initiative              q4-pricing-launch, webinar-onboarding
utm_content  = the variant (optional)      carousel-a, headline-b

Lowercase every UTM value and use hyphens, because LinkedIn and linkedin are two sources to a report. Leave click IDs such as gclid and li_fat_id alone, since they are case-sensitive. Fix the vocabulary once, and prefer a fixed list of mediums to free text. Never add UTMs to links between pages on your own site, because they overwrite the original source. Tag every link you control, and generate links from a template or small script instead of typing them.

Capture into the CRM

Analytics tools show traffic. Your CRM needs the source on the person at the moment they raise a hand. Store the parameters on arrival, then write them into hidden form fields on submit.

const KEYS = ["utm_source", "utm_medium", "utm_campaign", "utm_content", "gclid", "li_fat_id"];
 
function readParams() {
  const url = new URL(window.location.href);
  const found = {};
  for (const key of KEYS) {
    const value = url.searchParams.get(key);
    if (value) found[key] = key.startsWith("utm_") ? value.toLowerCase() : value;
  }
  return found;
}
 
function saveTouches() {
  const params = readParams();
  if (Object.keys(params).length === 0) return;
  try {
    const first = localStorage.getItem("first_touch");
    if (!first) localStorage.setItem("first_touch", JSON.stringify(params));
    localStorage.setItem("latest_touch", JSON.stringify(params));
  } catch (_) {
    /* storage can be blocked; the form still works without it */
  }
}
 
function fillHiddenFields(form) {
  let first = {};
  let latest = {};
  try {
    first = JSON.parse(localStorage.getItem("first_touch") || "{}");
    latest = JSON.parse(localStorage.getItem("latest_touch") || "{}");
  } catch (_) {}
  for (const [key, value] of Object.entries(first)) {
    const field = form.querySelector(`[name="first_${key}"]`);
    if (field) field.value = value;
  }
  for (const [key, value] of Object.entries(latest)) {
    const field = form.querySelector(`[name="latest_${key}"]`);
    if (field) field.value = value;
  }
}
 
saveTouches();
document.querySelectorAll("form").forEach((form) => {
  form.addEventListener("submit", () => fillHiddenFields(form));
});

Create matching CRM properties (first_utm_source, latest_utm_source and so on) and map the form fields to them. Check your consent setup before you store anything, because depending on where visitors live, storing identifiers may need consent first. Linking anonymous visits to the person who later fills in a form is a separate problem, covered in customer journey tracking.

Add one open question to demo and contact forms: "How did you hear about us?" Make it free text. A dropdown limits people to the channels you already know. Ask the same question on the first sales call and store that answer in its own property. It must never overwrite the tracked source.

Then copy both views onto the opportunity when it is created and lock them: tracked first-touch and latest-touch source and campaign, self-reported source (raw and categorized), the contact roles, and whether the deal came from outbound, inbound or a referral. If you use HubSpot, its attribution reporting documentation lists the built-in models and which reports support them. In another CRM, a few custom fields and a report get you most of the way.

Reconcile both views monthly

Tracked path: UTMs and click IDs flow into hidden fields and a tracked source. Self-reported path: an open question flows into a reported source. Both feed a monthly reconciliation.
Two views of the same lead, compared once a month

One query, run monthly, shows how they line up. It assumes an opportunities table with the fields above.

SELECT
  coalesce(o.self_reported_category, 'unknown') AS self_reported,
  coalesce(o.first_utm_source, 'untracked')     AS tracked_first_touch,
  count(*)                                      AS opportunities,
  count(*) FILTER (WHERE o.stage = 'closed_won') AS won,
  sum(o.amount) FILTER (WHERE o.stage = 'closed_won') AS won_amount
FROM opportunities o
WHERE o.created_at >= date_trunc('quarter', now()) - interval '1 quarter'
GROUP BY 1, 2
ORDER BY opportunities DESC;

Read it for patterns and not for exact figures. A large group that is "untracked" in one column and named a podcast in the other tells you the podcast works and your tracking cannot see it. A channel with many tracked clicks and no self-reported mentions may be collecting credit for demand created elsewhere. Keep a short decision log next to the report: what you saw, what you decided, what to check next time.

Where multi-touch stops helping

Multi-touch models divide credit across the touches you have recorded. HubSpot's position-based model, which it calls U-shaped, gives 40% of credit to the first interaction and 40% to the lead conversion interaction, with the rest spread over the others. Forty-forty-twenty is a convention, and a different convention gives different answers from the same data. A model also divides only the touches you can see. If the deciding touch was a conversation, no model finds it. With a few deals a month, one unusual deal can swing a channel's number.

Platform defaults have narrowed as well. Google Analytics 4 now offers data-driven, paid and organic last click, and Google paid channels last click. The first click, linear, time decay and position-based models have been unavailable there since November 2023, so other models mean building them from exported data. That export has its own limit: standard properties are capped at 1 million events per day in the BigQuery export.

We would not move budget on a multi-touch model at this volume. Use it as a second opinion: if a spreadsheet model on closed-won deals agrees with first-touch and self-reported views, you can be more confident. For a bigger decision, pause a channel in one region or segment for a few weeks and watch pipeline, which tells you more than any model.

When the numbers look off

SymptomLikely causeFix
Direct and unknown dominateMissing UTMs on email and newsletters, links shared in messaging apps, redirects that strip parametersAudit untagged links; test redirects
Source changes after the factA form fill overwrites the original sourceSeparate first-touch and latest-touch fields; lock the first
Sales distrusts the numbersThe report hides what reps hear on callsShow reps the self-reported column
Nobody acts on the reportIt does not map to a decisionCut every chart that supports no decision

Once source data is clean, you can use it for routing and follow-up. Pipeline automation covers how, and the lead scoring pipeline covers scoring the leads that come through.

Related Content