UTM Naming Convention: Rules, Allowed Values and a Template
Part of the UTM handbook
A UTM naming convention is a written list of rules and allowed values that everyone tagging links follows. This one is built around GA4's channel rules: the values that work, the ones that break, and code to check them.
A UTM naming convention is a short, written set of rules and allowed values for utm_source, utm_medium, utm_campaign and utm_content, so that every link your team tags lands in the same report row and the right GA4 channel. The one below has seven rules, a list of mediums mapped to GA4's default channels, and code that rejects anything off the list.
It is part of the UTM parameters handbook, which explains what each parameter does. This page is about choosing the values.
The 7 rules
- Lowercase everything. GA4 reports
Instagramandinstagramas two sources. - One separator: the hyphen.
autumn-sale, neverautumn_saleorautumn sale. utm_sourceis the site, app or sender the click comes from:instagram,newsletter,bing,ana-lopez.utm_mediumis the channel type, picked from the allowed list below. Nothing else, ever.utm_campaignis<yyyy-mm>-<offer>: the launch month, then a short name.2026-10-autumn-sale.utm_contentis the placement or creative:bio-link-2,story-2026-10-14,hero-button.- One owner keeps the list. A new source or medium goes on the list before it goes on a link.
Rule 1 is Google's own advice. Its URL builders page says "Parameter values are case sensitive, e.g. utm_source=google is different from utm_source=Google" and recommends lowercase "as a standard practice". GA4's channel rules ignore case ("Channel definitions aren't case sensitive"), so a capital letter splits your rows without changing the channel. That makes it easy to miss.
GA4 doesn't require rule 2; it's our pick. Hyphens win because two of the medium values in Google's channel rules use them (social-media, social-network) and they read cleanly in URLs. Google's own examples use underscores (spring_sale, paid_social), which is fine too. Mixing them is what hurts: paid-social and paid_social land in the same channel as two separate rows.
Allowed utm_medium values and their GA4 channel
The medium decides most of the channel. These values each match a rule in Google's default channel group (checked 15 October 2026):
utm_medium | GA4 default channel | Use it for |
|---|---|---|
email | Newsletters, drips, transactional emails | |
social | Organic Social | Posts, bio links, Stories, DMs, WhatsApp, community posts |
paid-social | Paid Social (source on Google's social list), else Paid Other | Ads on Meta, LinkedIn, TikTok, Pinterest, Reddit |
cpc | Paid Search (source on Google's search list), else Paid Other | Manually tagged search ads, e.g. Microsoft Ads |
display | Display | Banner and native placements on publisher sites |
video | Organic Video | YouTube descriptions, Vimeo, Twitch |
paid-video | Paid Video (source on Google's video list), else Paid Other | Video ads not auto-tagged |
sms | SMS | Text messages |
audio | Audio | Podcast show notes, audio ads |
affiliate | Affiliates | Creators and partners paid per sale |
referral | Referral | Partner sites, directories, guest posts |
push | Mobile Push Notifications | App and web push |
qr | Unassigned by default | Posters, flyers, packaging. Add a custom channel for it (how) |
qr is the one deliberate exception. GA4 has no print channel, so every accurate value for a poster lands in Unassigned. One agreed value plus a custom channel beats spreading print across referral and social.
Values that break
Each of these is a real-looking value with the channel it gets. Read from Google's rules, as of 15 October 2026:
| Value | What happens | Use instead |
|---|---|---|
utm_medium=newsletter | Unassigned (no rule mentions it), unless the source is on one of Google's lists | email |
utm_medium=email-newsletter | Unassigned (Email needs an exact match), unless the source is on one of Google's lists | email |
utm_medium=organic-social | Unassigned unless the source is on the social list | social |
utm_medium=story, bio, post | Unassigned unless the source is on the social list | social + utm_content=story-… |
utm_medium=influencer | Unassigned, unless the source is on one of Google's lists | affiliate if paid per sale, else social |
utm_medium=podcast | Unassigned, unless the source is on one of Google's lists | audio |
utm_medium=cost-per-click | Not paid (the paid pattern wants cp together): Organic Search with a search-engine source, Organic Social with a social one, else Unassigned | cpc |
utm_medium=social-paid | Not paid: paid.* only matches at the start | paid-social |
utm_medium=mobile-social | Matches Mobile Push Notifications (contains "mobile") | social |
utm_medium=cpm with a social source | Fits Paid Social, Display, Paid Other and Organic Social at once | paid-social |
utm_source=whatsapp + utm_medium=sms | Fits Organic Social and SMS | whatsapp + social |
Note what isn't on the list: e-mail. Google's Email rule accepts email, e-mail, e_mail and e mail, so it works. It just splits your rows from email.
utm_source values for each network
The source matters most when the medium is paid: Paid Social needs a source on Google's list of social sites. That list is a PDF of sources and categories, dated 9 February 2023, with 592 social entries. We checked each value below against it:
| Network | utm_source | On Google's list? | Note |
|---|---|---|---|
instagram | Social | Google's best-practice table suggests insta, which is not on the list | |
facebook | Social | fb is on the list too; pick one | |
| TikTok | tiktok | Social | |
linkedin | Social | Posts, first comments and colleagues' shares: LinkedIn link tracking | |
pinterest | Social | ||
reddit | Social | ||
whatsapp | Social | ||
| Snapchat | snapchat | Social | |
| YouTube | youtube | Video | Pair with video or paid-video, not social |
| X | x | No | Fine with social. For X ads use twitter, which is on the list |
| Threads | threads | No | Fine with social; paid lands in Paid Other |
| Bluesky | bsky | No | Fine with social |
| Telegram | telegram | No (t.me is) | Fine with social |
| Discord | discord | No | Fine with social |
your list name: newsletter, onboarding-drip | n/a | The medium email does the work | |
| SMS | your list or sender: text-club | n/a | The medium sms does the work |
the object: poster, flyer, pack | n/a | With qr |
Google doesn't say whether the social list is matched exactly or by substring, so stick to the plain names above.
Campaign names that sort
Use <yyyy-mm>-<offer>: 2026-10-autumn-sale, 2026-11-black-friday, 2027-01-pricing-change. Any list of campaigns then sorts by launch date, and two years of autumn sales never collide.
Two words to keep out of campaign names, because GA4 reads the campaign for two channels:
- "cross-network" sends the session to the Cross-network channel.
- "shop" or "shopping" can send it to Organic or Paid Shopping. The pattern is
^(.*(([^a-df-z]|^)shop|shopping).*)$: "shopping" anywhere, or "shop" at the start, after any character that isn't a letter (a hyphen, a digit, a dot) or after the letter e.2026-11-shop-launchandeshop-tourmatch.2026-11-workshopdoesn't, because a k comes before "shop".
Keep the campaign name identical across every channel. The source and medium tell the channels apart; the campaign ties them together.
utm_content: name the placement
utm_content answers "which one?". Name the place or the creative, and keep the pattern fixed per channel:
| Channel | Pattern | Examples |
|---|---|---|
| link position | hero-button, footer-link, ps-link | |
| Bio links | slot number | bio-link-1, bio-link-2 |
| Stories | story-<yyyy-mm-dd> | story-2026-10-14 |
| Posts | post type | pinned-post, carousel, reel |
| Ads | ad name in the ad manager | carousel-a, video-ad-1 |
| location | store-window, market-stall, pack-insert |
Lint your tags
A convention only holds if something checks it. For one link at a time, use the UTM builder that flags capitals and spaces and mediums that end in Unassigned. For a pipeline, these reject a tagged URL that's missing a required parameter, has a value that isn't lowercase-and-hyphens, or uses a medium off the list.
const ALLOWED_MEDIUMS = new Set([
"email", "social", "paid-social", "cpc", "display", "video", "paid-video",
"sms", "audio", "affiliate", "referral", "push", "qr",
]);
const VALUE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
function lintUtm(url) {
const params = new URL(url).searchParams;
const problems = [];
for (const key of ["utm_source", "utm_medium", "utm_campaign"]) {
if (!params.get(key)) problems.push(`missing ${key}`);
}
for (const [key, value] of params) {
if (key.startsWith("utm_") && !VALUE.test(value)) problems.push(`${key}="${value}" is not lowercase-and-hyphens`);
}
const medium = params.get("utm_medium");
if (medium && !ALLOWED_MEDIUMS.has(medium)) problems.push(`utm_medium="${medium}" is not on the allowed list`);
if (/(^|[^a-df-z])shop|shopping|cross-network/.test(params.get("utm_campaign") ?? "")) {
problems.push("utm_campaign may trigger a Shopping or Cross-network channel");
}
return problems;
}
console.log(lintUtm("https://example.com/autumn-sale?utm_source=Instagram&utm_medium=story&utm_campaign=2026-10-autumn-sale").join("\n"));
// utm_source="Instagram" is not lowercase-and-hyphens
// utm_medium="story" is not on the allowed listThe same in Python, for a list of URLs in a file:
import re
import sys
from urllib.parse import parse_qsl, urlsplit
ALLOWED_MEDIUMS = {"email", "social", "paid-social", "cpc", "display", "video", "paid-video",
"sms", "audio", "affiliate", "referral", "push", "qr"}
VALUE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
def lint_utm(url):
params = dict(parse_qsl(urlsplit(url).query))
problems = [f"missing {k}" for k in ("utm_source", "utm_medium", "utm_campaign") if not params.get(k)]
problems += [f'{k}="{v}" is not lowercase-and-hyphens'
for k, v in params.items() if k.startswith("utm_") and not VALUE.match(v)]
medium = params.get("utm_medium")
if medium and medium not in ALLOWED_MEDIUMS:
problems.append(f'utm_medium="{medium}" is not on the allowed list')
if re.search(r"(^|[^a-df-z])shop|shopping|cross-network", params.get("utm_campaign", "")):
problems.append("utm_campaign may trigger a Shopping or Cross-network channel")
return problems
# Usage: python3 lint_utm.py urls.txt (one URL per line)
for line in open(sys.argv[1]):
url = line.strip()
if url and (problems := lint_utm(url)):
print(url, *problems, sep="\n ")In Google Sheets, a dropdown keeps people on the list in the first place. Put the allowed mediums in a range (say Lists!A2:A14), select the medium column, then Data → Data validation → Add rule → Dropdown (from a range) and choose Reject the input under the advanced options. For free-text columns, a check column flags bad values:
=IF(REGEXMATCH(B2, "^[a-z0-9]+(-[a-z0-9]+)*$"), "", "fix: " & B2)Fix old messy data
GA4 can't rewrite values it has already collected. What it can do is group them differently. A custom channel group puts your own rules on top of the same data, and Google's custom channel groups page says they "can be applied to your reports retroactively".
Use one to send last year's newsletter and Newsletter mediums to Email, or to give qr its own channel. The steps are on the GA4 Unassigned guide. Then fix the links themselves, so the custom rules only have stragglers left to catch.
How the slug and the UTMs fit together
A short link has two names, and they do different jobs:
- The slug is for people.
snu.to/autumn-ig-bio2can be read on a poster, typed from a podcast and recognised in a bio. It follows its own rules (1 to 32 letters, digits,-or_; case-sensitive; can't be changed later), covered in the short link slugs guide. - The UTMs are for GA4. They go on the destination URL the slug redirects to, following this convention.
Don't put UTM values in the slug or try to make them match. Keep one short link per placement and give it the UTMs of that placement; the short link then counts clicks per placement on its own, and GA4 gets clean source, medium and campaign values. The social media link tracking guide has a full sheet for a campaign set up that way.
The convention template
Paste this into a sheet (File → Import, or paste and use Split text to columns). The first block is the allowed-values list; the second is one campaign filled in.
list,value,ga4_channel,notes
medium,email,Email,
medium,social,Organic Social,
medium,paid-social,Paid Social,source must be on Google's social list
medium,cpc,Paid Search,source must be on Google's search list; Google Ads uses auto-tagging
medium,display,Display,
medium,video,Organic Video,
medium,paid-video,Paid Video,source must be on Google's video list
medium,sms,SMS,
medium,audio,Audio,
medium,affiliate,Affiliates,
medium,referral,Referral,
medium,push,Mobile Push Notifications,
medium,qr,Unassigned,custom channel group: medium exactly matches qr
source,instagram,,
source,facebook,,
source,tiktok,,
source,linkedin,,
source,youtube,,
source,x,,use twitter for X ads
source,newsletter,,
source,poster,,placement,short_link,utm_source,utm_medium,utm_campaign,utm_content
Instagram bio link 2,snu.to/autumn-ig-bio2,instagram,social,2026-10-autumn-sale,bio-link-2
Instagram Story 14 Oct,snu.to/autumn-ig-story,instagram,social,2026-10-autumn-sale,story-2026-10-14
Newsletter hero button,snu.to/autumn-news-hero,newsletter,email,2026-10-autumn-sale,hero-button
Meta carousel ad,(none: tagged URL in the ad),facebook,paid-social,2026-10-autumn-sale,carousel-a
Store window poster,snu.to/autumn-poster,poster,qr,2026-10-autumn-sale,store-window
Podcast episode 112,snu.to/autumn-pod,making-it-podcast,audio,2026-10-autumn-sale,ep-112-show-notesBuild the tagged URLs from the second block with the Sheets formula in the UTM parameters guide, or paste the destinations into the bulk UTM builder, which runs the same capital, space and channel checks on every row.
Checklist for adopting it
- The allowed
utm_mediumlist is written down and one person owns it - Every value is lowercase, words joined with hyphens
- Each network has exactly one
utm_sourcevalue - Campaign names start with
yyyy-mmand avoid "shop" and "cross-network" utm_contentfollows one pattern per channel- The medium column in your sheet is a dropdown, and the lint runs before launch
qrhas a custom channel in GA4, and old messy values are grouped there too- UTMs go on the destination, never in the short link's slug
Frequently asked questions
Should UTMs be lowercase?
Yes. GA4 treats utm_source=Instagram and utm_source=instagram as two different sources in reports, and Google's own best-practice table says to use lowercase as a standard practice.
Hyphens or underscores in UTMs?
Either works in GA4. Pick one and never mix them, because paid-social and paid_social become two report rows. Hyphens match the medium values in Google's own channel rules (social-media, social-network), so this guide uses hyphens.
What should utm_medium be for Instagram?
social for organic posts, bio links and Stories, and paid-social for ads, with utm_source=instagram. Both land in the channel you expect: Organic Social and Paid Social.
How do you name UTM campaigns?
Start with the year and month the campaign launches, then a short name for the offer: 2026-10-autumn-sale. The names sort by date, and the same campaign keeps the same name on every channel.
Start tracking links free.
Create your first short link with click analytics in seconds. No credit card, no sign-up to start.
Shorten a link