UTM parameters are small additions to a link that tell your analytics where a visit came from:
https://example.com/pricing?utm_source=newsletter&utm_medium=email&utm_campaign=spring-sale-2025&utm_content=header-button
They are a great idea until six people use them for a year without agreeing on spelling. Then the channel report lists email, Email, e-mail and newsletter as four different mediums and nobody can say how e-mail performed.
The five parameters
| Parameter | Answers | Example |
|---|---|---|
utm_source |
Where the visit came from | newsletter, linkedin, google |
utm_medium |
What kind of channel | email, cpc, social |
utm_campaign |
Which initiative | spring-sale-2025 |
utm_content |
Which link or creative, when there are several | header-button, banner-b |
utm_term |
The paid keyword (search campaigns) | running-shoes |
The first three are the ones to require. The other two earn their place when you need to compare links in the same message or ads in the same campaign.
The rules, and why each exists
The checklist above is the rulebook. The reasons are practical:
- One spelling. Analytics tools match values as exact text, so every variant is a separate row. Lower case and hyphens remove most variants.
- A closed list for medium. Default channel groupings in analytics tools are built from the medium. A medium nobody expected ends up in "Unassigned" or "Other".
- No tags on internal links. A tagged link on your own site tells the analytics tool that a new visit has started, from a source that is your own website. It destroys the real origin of the session.
- No personal data in URLs. Links are copied into e-mails, chats and server logs. Anything in the URL should be assumed public.
Enforce it with code
A rule that depends on memory fails. This script builds tagged links, normalises the values and refuses a medium that is not on the list.
import re
from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse
ALLOWED_MEDIUMS = {"email", "cpc", "social", "referral", "affiliate", "display"}
def clean(value: str) -> str:
"""lower case, words joined by hyphens, nothing but letters, digits and hyphens"""
value = re.sub(r"[^a-z0-9]+", "-", value.strip().lower())
return value.strip("-")
def tag_url(url: str, source: str, medium: str, campaign: str, content: str = "", term: str = "") -> str:
medium = clean(medium)
if medium not in ALLOWED_MEDIUMS:
raise ValueError(f"medium {medium!r} is not one of {sorted(ALLOWED_MEDIUMS)}")
parts = urlparse(url)
query = dict(parse_qsl(parts.query)) # keep any parameters the link already has
query.update({"utm_source": clean(source), "utm_medium": medium, "utm_campaign": clean(campaign)})
if content:
query["utm_content"] = clean(content)
if term:
query["utm_term"] = clean(term)
return urlunparse(parts._replace(query=urlencode(query)))
print(tag_url("https://example.com/pricing", "Newsletter", "Email", "Spring Sale 2025", content="Header Button"))
print(tag_url("https://example.com/landing?ref=partner", "LinkedIn", "social", "Launch / Webinar!"))
try:
tag_url("https://example.com", "x", "e-mail", "y")
except ValueError as error:
print(error)
https://example.com/pricing?utm_source=newsletter&utm_medium=email&utm_campaign=spring-sale-2025&utm_content=header-button
https://example.com/landing?ref=partner&utm_source=linkedin&utm_medium=social&utm_campaign=launch-webinar
medium 'e-mail' is not one of ['affiliate', 'cpc', 'display', 'email', 'referral', 'social']
Three things to notice. Messy input (Spring Sale 2025, Launch / Webinar!) becomes clean values. A link that already carries a parameter (ref=partner) keeps it. And e-mail is rejected rather than guessed at, so the person building the link has to pick email and the reports stay clean.
Keep a registry
A shared sheet with one row per campaign (name, owner, dates, the exact UTM values, the landing page) turns tagging from guesswork into a record. It also answers the question that otherwise arrives six months later: "what was summer-promo-b?"
Check before you celebrate
After launching a campaign, click a tagged link yourself in a private window. Within a few minutes, the visit should appear in the real-time report under the source and medium you intended. A broken link found in the first hour is a typo; found a month later it is a month of missing data.
Checklist
- Use all of utm_source, utm_medium and utm_campaign on every campaign link. Add utm_content to tell apart links in the same message, and utm_term for paid keywords.
- Write everything in lower case. Reports treat 'Email' and 'email' as different values, and nobody remembers which one they used last time.
- Use hyphens instead of spaces, and avoid other punctuation. Spaces become %20 and make links ugly and error-prone.
- Choose utm_medium from a short, fixed list (for example email, cpc, social, referral, affiliate, display) and let nothing else in. Mediums drive your channel reports.
- Make utm_source the specific origin (newsletter, linkedin, google) and utm_campaign the initiative (spring-sale-2025). Put the date or year in campaign names so they stay unique.
- Never add UTM parameters to links inside your own site. They start a new session and replace the real source of the visit. Track internal clicks with events.
- Never put personal data (an e-mail address, a name, a customer id) in a link. URLs are logged, shared and cached everywhere.
- Keep a shared registry of every source, medium and campaign in use, and one owner who approves new values.
- Generate links with a tool or script, not by hand, so the rules are enforced and not merely written down.
- Check campaigns after launch: click a tagged link yourself and confirm the visit appears under the intended source and medium.
Frequently asked questions
Do UTM parameters work for every analytics tool?
They are a convention, not a standard. Google Analytics reads them by default, and most other web analytics tools also understand utm_source, utm_medium and utm_campaign, but check yours.
What about links that are shared or copied?
A shared link keeps its parameters, so a tagged link posted by a reader attributes their friends' visits to your campaign. Use a short link for the public, and tagged links for the channels you control.
References
From Grokem: https://www.grokem.com/data/data-analytics/marketing-measurement/campaign-tracking/utm-parameters/utm-naming-conventions




