Skip to content
Free Tool Arena

How-To & Life · Guide · Developer Utilities

How to parse UTM parameters

Decoding all five UTM fields, auditing inbound links, handling URL encoding edge cases, and debugging attribution gaps.

Updated April 2026 · 6 min read

Reading UTM parameters off an incoming URL sounds like a one-liner — new URLSearchParams(location.search).get('utm_source') and you’re done — but using UTMs well means deciding what to do with them: when to store, when to overwrite, when to expire, and how to build an attribution chain without leaking PII into your analytics. The five parameters arrive clean from the URL but they need to be promoted to first-party cookies or local storage for later conversions, and the choice between first-touch and last-touch attribution drives which campaigns get credit. This guide covers the parsing API, building an attribution chain, the first-touch vs last-touch decision, how GA4 handles the same problem under the hood, the common server-side parsing mistakes, and the privacy considerations when UTMs end up in logs.

Advertisement

Extracting UTMs from the URL

Modern browsers and Node give you URLSearchParams for free.

const params = new URLSearchParams(window.location.search);
const utms = {
  source:   params.get('utm_source'),
  medium:   params.get('utm_medium'),
  campaign: params.get('utm_campaign'),
  term:     params.get('utm_term'),
  content:  params.get('utm_content'),
};

get() returns null if the parameter is absent. URL decoding is automatic — a utm_campaign=q1%20launch comes back as "q1 launch".

When to read, when to store

Read the UTMs on the first page load of a session. If any are present, stash them. If none are present, keep whatever was stored previously. Two stores you typically need:

Session-scoped (current visit): the most recent UTM set the user arrived with. Used for immediate attribution on this visit’s conversion.

First-touch (ever): the very first UTM set this user ever arrived with, written once and never overwritten. Lives until the user clears cookies or your retention window expires (90 days is common).

function captureUtms() {
  const p = new URLSearchParams(location.search);
  const keys = ['utm_source','utm_medium','utm_campaign',
                'utm_term','utm_content'];
  const found = {};
  keys.forEach(k => {
    const v = p.get(k);
    if (v) found[k] = v;
  });
  if (Object.keys(found).length === 0) return;

  sessionStorage.setItem('last_touch', JSON.stringify(found));
  if (!localStorage.getItem('first_touch')) {
    localStorage.setItem('first_touch', JSON.stringify(found));
  }
}

First-touch vs last-touch vs multi-touch

First-touch gives full credit to the campaign that first introduced the user. Great for top-of-funnel evaluation. Blind to nurture campaigns that actually drove the conversion.

Last-touch gives credit to the final click before conversion. Easy to calculate, but tends to over-credit retargeting and branded search.

Linear or position-based multi-touch spreads credit across all touches. Closer to reality, harder to implement, and needs a stitched user journey across sessions.

For most SaaS: capture both first and last touch, report both, and let revenue ops argue about the ratio. Capturing both costs nothing extra at pageview time.

The attribution chain

A user might visit your site six times before converting, each visit with or without a UTM set. The sequence looks like:

visit 1: utm_source=reddit    medium=social     campaign=launch_post
visit 2: (direct, no UTMs)
visit 3: utm_source=newsletter medium=email      campaign=tip_tuesday
visit 4: (direct)
visit 5: utm_source=google     medium=cpc        campaign=brand
visit 6: (direct) -> conversion

First-touch credits reddit / social. Last-touch (of non-direct) credits google / cpc. Both can be valuable; neither is complete. To build the full chain, store an append-only array in local storage or on the server, capped at a reasonable length (10–20 touches) to avoid unbounded growth.

Server-side parsing

Server parsing follows the same URL rules but comes with two traps. First, frameworks like Express already decode query parameters — reading req.query.utm_sourcegives you a decoded string, so decoding again is double-decoding. Second, when your app sits behind a CDN, make sure the CDN forwards the query string intact; some caching rules strip parameters before origin sees them.

app.get('/landing', (req, res) => {
  const { utm_source, utm_medium, utm_campaign,
          utm_term, utm_content } = req.query;
  if (utm_source) {
    logTouch(req.cookies.anon_id, {
      source: utm_source,
      medium: utm_medium,
      campaign: utm_campaign,
      ts: Date.now()
    });
  }
  res.render('landing');
});

Handing UTMs off to your analytics tool

If you run GA4, Amplitude, Mixpanel, or similar, they read UTMs from the URL automatically — you don’t need to forward them. Your stored UTMs are extra, for internal dashboards, CRM enrichment, and server-side event logs that the client-side tag cannot reach.

When sending to your CRM, map the five UTMs onto named fields (e.g., HubSpot’s hs_analytics_source) on lead creation. Set them once; do not overwrite on subsequent visits unless your model is pure last-touch.

Cross-domain and subdomain stitching

Local storage is per-origin, so a user flowing from www.example.com to app.example.com loses their stored UTMs. Three options:

Forward UTMs in the link between subdomains. Good for a narrow handoff (marketing site → signup form).

Share a parent-domain cookie(Domain=.example.com). Works across subdomains but not across completely different domains.

Send the user a stable ID that both sides share, and attribute server-side. The most reliable approach but the most work.

Privacy considerations

UTM parameters themselves are not personal data, but they sit on URLs that are logged, indexed, and shared. A UTM like utm_content=abandoned_cart_24h_doug leaks a user name into logs if the template is careless. Two rules:

Never put PII — name, email, phone, account ID — into UTM values. The convenience is not worth the compliance risk (GDPR, CCPA).

Do strip UTMs from the visible URL after capture, using history.replaceState, to prevent them from being copy-pasted onward and to keep the URL clean for canonical purposes.

Common mistakes

Overwriting first-touch on every visit.Writing to the first-touch store unconditionally erases it. Check for existence before setting.

Double decoding. Query parameters come out of URLSearchParams or req.query already decoded. Calling decodeURIComponent on "q1 launch" again is a no-op unless the value happened to contain a %.

Tagging for campaigns that never launched.Old UTM links lie around forever. A well-meaning customer success rep links to a year-old landing URL and that campaign lights up in today’s report. Expire old URLs, or version campaign names with the year.

Stripping UTMs before reading them. Some security libraries canonicalize URLs on ingress; if they run before your capture code, the parameters are already gone. Order matters.

Ignoring referrer for attribution gap-fill.When UTMs are missing, document.referrer often has useful signal (facebook.com, t.co, google.com). Pair the two sources.

Logging full URLs with UTMs into error reports.UTM values leak campaign strategy and sometimes PII into third-party error tracking. Scrub URLs before sending them off.

Run the numbers

Paste any URL into the UTM parser to see exactly how your code will read it. Pair with the query string parser to decompose the full parameter set when a page has non-UTM tracking too, and the URL cleaner for producing the UTM-free version of the URL you want users to share.

Advertisement

Found this useful?Email