A UTM parameter is just a query string your analytics tool knows how to read. Append it to a link, and when someone taps that link the visit gets filed under a source and campaign you chose instead of landing in the great undifferentiated pile of direct traffic.
This matters more for messaging than for any other channel. Email clients pass a referrer. Search engines pass a referrer. A link tapped inside iMessage generally does not — so without tags, the single highest-converting channel a small business owns is invisible in its own dashboard.
The five parameters
- utm_source — where the traffic came from. Use
imessage, not the provider name. You may switch providers; the channel stays. - utm_medium — the kind of channel. Keep
smsfor anything that lands in the Messages app, so iMessage and SMS fallback aggregate into one honest number. - utm_campaign — the specific push.
spring_service_special,invoice_reminder,review_request_v2. - utm_content — which variation. Use it to A/B two wordings of the same campaign.
- utm_term — optional. In messaging, most teams use it for audience segment:
lapsed_90d,vip,new_customer.
Lowercase everything, always
Most analytics tools treat Spring_Special and spring_special as two different campaigns. You will not notice until you are staring at a report that splits your numbers in half. Normalise to lowercase in code, not in a spreadsheet convention nobody follows.
Build the URL in code, never by hand
Hand-typed UTMs drift within about two weeks. One helper function, used everywhere, is the whole discipline. The UTM builder generates the same thing interactively if you want to see it first.
type Tags = { campaign: string; content?: string; term?: string; source?: string; medium?: string;}; export function tagged(url: string, tags: Tags) { const u = new URL(url); const norm = (v: string) => v.trim().toLowerCase().replace(/\s+/g, "_"); u.searchParams.set("utm_source", norm(tags.source ?? "imessage")); u.searchParams.set("utm_medium", norm(tags.medium ?? "sms")); u.searchParams.set("utm_campaign", norm(tags.campaign)); if (tags.content) u.searchParams.set("utm_content", norm(tags.content)); if (tags.term) u.searchParams.set("utm_term", norm(tags.term)); return u.toString();}A naming scheme that survives a team
Pick a convention now and write it down, because the cost of inconsistency compounds. The one that holds up best is {trigger}_{intent} — what caused the send, and what you want them to do.
booking_confirm— they booked, confirm the details.appointment_reminder— 24h out, keep the slot.review_request— post-service, ask for a rating.winback_offer— lapsed, bring them back.invoice_due— outstanding balance, get paid.
Keep the link short enough to trust
A fully tagged URL is long and slightly alarming inside a chat bubble. Two fixes, in order of preference:
- Use your own short domain with a redirect that carries the UTMs.
go.acme.co/springresolves to the tagged URL server-side. The customer sees a short branded link; your analytics still sees the tags. - Use a link shortener with a custom domain. Avoid generic shorteners — carrier spam filters treat shared shortener domains with real suspicion. More on that here.
import { redirect } from "next/navigation"; const LINKS: Record<string, { url: string; campaign: string }> = { spring: { url: "https://acme.co/offers/spring", campaign: "spring_service_special" }, book: { url: "https://acme.co/book", campaign: "booking_confirm" },}; export async function GET( _request: Request, { params }: { params: Promise<{ slug: string }> },) { const { slug } = await params; const link = LINKS[slug]; if (!link) redirect("/"); // Short link in the bubble, full attribution on arrival. redirect(tagged(link.url, { campaign: link.campaign }));}The redirect trick
A redirect to the tagged destination preserves attribution perfectly and lets you change the destination after the message has been sent. That alone is worth the twenty minutes it takes to set up.
Close the loop to revenue
Traffic is not the point. Set a conversion goal on the action that pays you — booking confirmed, invoice paid, order placed — and segment it by utm_campaign. That turns 'we sent 400 texts' into 'this campaign returned $3,100', which is the only version of the sentence anyone should act on. Measuring ROI covers the rest.
If you cannot say what a campaign earned, you cannot say whether to run it again. Tagging is what turns that from an opinion into a number.
Next step
Generate a tagged link for whatever you send next with the UTM builder, see what this looks like in your industry, or compare the services that can send it on the providers page.