Measurement
Tag every link you send, or the channel gets no credit
Taps from a message arrive with no referrer. Analytics files them as direct traffic — the same bucket as people who typed your domain from memory. The sale gets counted; the message that caused it does not.
Why messaging traffic disappears
When someone clicks a link on a web page, the browser sends a referrer header and your analytics knows where they came from. Taps inside a messaging app do not work that way. The Messages app is not a web page, so there is nothing to refer from — the visit arrives looking exactly like someone who opened a new tab and typed your address.
The consequence is specific and expensive: messaging drives a sale, your analytics credits direct / none, 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.
UTM parameters solve it by putting the attribution data in the URL itself. The customer taps a link that already says where it came from, and every analytics tool in common use — GA4, PostHog, Plausible, Shopify, whatever your ecommerce platform reports with — reads those parameters natively. No integration, no vendor cooperation required.
The five parameters
Three are effectively mandatory. Two are optional and should stay empty unless you have a specific question they answer.
| Parameter | Example | What it holds | How to use it |
|---|---|---|---|
| utm_sourceRequired | imessage | Where the traffic came from. | Use one value for the whole channel. If you send from three different lines, they are all still imessage — put the line in utm_content if you need to split them. |
| utm_mediumRequired | messaging | The category of channel. | Pick one and never deviate. messaging is the sane choice; sms is misleading if you are sending iMessage, and text groups badly against everything else in your reports. |
| utm_campaignRequired | appointment-reminder | The specific play or promotion. | Name it after the play, not the date. appointment-reminder every month gives you a trend line; appointment-reminder-march-2026 gives you twelve unrelated rows. |
| utm_contentOptional | variant-a | Which version of the message. | The A/B slot. Also useful for distinguishing two links inside the same message. |
| utm_termOptional | returning-customer | Originally paid-search keyword; free for you to repurpose. | Audience segment or location works well. Leave it empty rather than filling it with something you will not query. |
Tool
Build a tagged link
Presets match the plays on the use cases page, so the campaign names stay consistent between what you read here and what you send.
Tagged link builder
Values are normalized to lowercase with dashes, because Appointment Reminder and appointment-reminder become two separate rows in your reports otherwise.
Start from a play
Where the click came from. Keep this identical across every message you ever send.
The channel category. Use one value for all messaging so it groups in reports.
The specific play. Reuse the same name every time you run it.
Optional. Distinguishes variants — which wording, which link position.
Optional. A free slot — audience segment or location works well here.
Your tagged link
Add campaignDo this in code, not by hand
A builder is fine for a one-off. For anything recurring, put the tagging in the function that sends the message — see the example below. Hand-built links drift within about two weeks, and one capitalized campaign name splits a report in half.
Naming conventions that survive contact with reality
- Lowercase, always. Most analytics tools treat UTM values as case-sensitive.
- Dashes, not spaces or underscores. Spaces become %20 and read as a different value.
- No dates in campaign names. You want a trend line, not a new row every month.
- Write the list down. One shared doc of approved campaign names, and everyone picks from it.
In code
Attach the tags where the message is built
One helper function, called by every send. After this, nobody on your team has to remember anything.
The example uses Sendblue’s documented endpoint, but the pattern is provider-agnostic — the tagging happens before the request body is assembled, so it works the same whichever API you send through.
// One helper, used by every send. Tags stop being something
// anyone has to remember.
function taggedLink(url, campaign, extra = {}) {
const link = new URL(url);
link.searchParams.set("utm_source", "imessage");
link.searchParams.set("utm_medium", "messaging");
link.searchParams.set("utm_campaign", campaign);
for (const [key, value] of Object.entries(extra)) {
link.searchParams.set(`utm_${key}`, value);
}
return link.toString();
}
await fetch("https://api.sendblue.co/api/send-message", {
method: "POST",
headers: {
"sb-api-key-id": process.env.SENDBLUE_KEY_ID,
"sb-api-secret-key": process.env.SENDBLUE_SECRET,
"Content-Type": "application/json",
},
body: JSON.stringify({
number: customer.phone,
content: `Hi ${customer.firstName} — your 2pm Thursday slot is confirmed. ` +
`Details and directions: ${taggedLink("https://acme.co/visit", "appointment-reminder")}`,
}),
});Credentials come from environment variables, never from the client. An API key in browser-shipped code is a key you have given away.
Gotchas
Four things that quietly break attribution
Every one of these has cost somebody a quarter of clean data.
Redirects that drop query strings
If your link goes through a redirect — a short link, a www-to-apex rule, an old URL — check that the UTM parameters survive the hop. Plenty of redirect configurations strip the query string silently. Test the real link on a real phone before you send it to a thousand people.
Link previews inflating your clicks
iMessage fetches a URL to render its preview card. That fetch can show up in your analytics as a visit nobody made. If your numbers look impossibly good, this is usually why — filter out requests with no engagement, or compare against server-side conversions.
Tags on the wrong link
If your message contains two links, tag both, and give them different utm_content values. Otherwise you know the message worked but not which half of it did.
Attribution windows that expire
Someone taps on Tuesday and buys on Friday from a bookmark. Whether that sale credits messaging depends on your analytics attribution window and cookie lifetime — which varies by tool and by browser. Know what yours is before you conclude the channel underperformed.
Set the tagging up before the first send
Attribution cannot be added retroactively. The messages you send in week one are the ones you will most want to evaluate — tag them now, not after somebody asks whether the line is paying for itself.
Further reading
Measuring the channel properly
UTM tracking that survives
Naming conventions, where to put the tagging, and the four failure modes that quietly destroy messaging attribution — written for people who will still be running this next year.
8 min readMeasurementCalculating messaging ROI
A worked model for deciding whether a line pays for itself, and the three mistakes that make most messaging ROI calculations meaningless.
8 min readMeasurementWhat read receipts tell you
Delivered and read are different numbers, and the gap between them is the most useful diagnostic a small business can have about its own messaging.
7 min readMeasurementA/B testing your messages
Small businesses do not have the volume for classical significance testing. Here is what you can legitimately learn from a few hundred sends, and what you cannot.
7 min readOperationsConnecting iMessage to your CRM
The integration decisions that determine whether your messaging data is useful in a year — where conversations live, what syncs, and what must never diverge.
8 min readOperationsOne number or many?
The decision looks administrative and is actually strategic. It determines what your reporting can tell you and how customers experience the brand.
6 min read