Taps from a messaging app arrive with no referrer. Analytics files them as direct traffic — the same bucket as someone who typed your domain from memory. The sale gets counted and the message that caused it does not, and at the end of the quarter the channel looks like it produced nothing.
Businesses cancel lines over this. The channel was working; the reporting was blind. The mechanics of fixing it are on the UTM tracking guide, including a link builder. This article is about the part that goes wrong *after* you have set it up correctly.
Drift is the real enemy
A tagging scheme rarely breaks all at once. It degrades:
- Somebody builds a link by hand and types
Appointment Reminderinstead ofappointment-reminder. Most analytics tools treat these as different values, and your report now has two rows for one play. - A second person picks
appt-reminderbecause it seemed clearer. - Somebody adds
-marchto the campaign name, so month-over-month comparison quietly stops existing. - A new play launches with
utm_medium=smsbecause that is what the person knew, and it groups separately from everything else.
Six months later the reporting is unusable and nobody can say when it stopped being useful. This is not a discipline problem you can solve by asking people to be careful.
The structural fix
Put the tagging in the function that sends the message. If a human never types a UTM value, a human never mistypes one. Everything else — shared docs, naming guidelines, spreadsheets of approved campaigns — is a mitigation for having got this wrong.
The conventions worth enforcing
| Parameter | Value | Rule |
|---|---|---|
| utm_source | imessage | One value forever. Never varies. |
| utm_medium | messaging | One value forever. Not 'sms', not 'text' — those group badly. |
| utm_campaign | appointment-reminder | Named after the play. Never contains a date. |
| utm_content | variant-a | Variants and multiple links in one message. Optional. |
| utm_term | returning-customer | Segment or location. Optional. |
Lowercase, dashes, no spaces. If you run several lines, they are all still utm_source=imessage — put the line identity in utm_content if you genuinely need to split them. Resist the urge to encode everything; a parameter you will never query is noise.
The four failure modes
1. Redirects that drop the query string
A short link, a www-to-apex rule, an old URL that forwards — plenty of redirect configurations strip query parameters silently. The link works, the page loads, the attribution vanishes. Test the real link on a real phone before it goes to a thousand people, and re-test whenever anyone touches your DNS or redirect rules.
2. Link previews inflating your numbers
iMessage fetches a URL to render its preview card. That fetch can appear in analytics as a visit nobody made. If your click numbers look impossibly good, this is usually why. Compare against server-side conversions rather than trusting page views, and be suspicious of a click-through rate that would be a record for any channel.
3. Two links, one tag
If a message contains two links, tag both with different utm_content values. Otherwise you know the message worked and not which half of it did — which is exactly the information you need to improve it.
4. Attribution windows you never checked
Someone taps on Tuesday and buys on Friday from a bookmark. Whether that credits messaging depends on your analytics attribution window and cookie lifetime, which vary by tool and are increasingly constrained by browsers. Know what yours is *before* you conclude the channel underperformed — this single unchecked setting has killed more messaging budgets than bad copy.
What good looks like
Source / Medium: imessage / messaging Last 90 days
campaign sessions conv. revenue
appointment-reminder 1,204 312 $56,160
review-request 486 91 —
waitlist-fill 203 74 $10,360
rebooking 177 52 $ 9,360
lead-response 141 19 $ n/a
5 campaigns. Same names as 90 days ago.
No case variants, no dated duplicates.If your report looks like this after a year, the scheme held.
Five rows, stable names, comparable across periods. That is the whole goal, and it is achievable by making it impossible to do otherwise. Build the tagging into the send — here is what that looks like in code.