Skip to content
imessageapi

A UTM naming convention your team will actually follow

Every analytics account eventually contains spring_sale, Spring-Sale and springsale2026. Here is the scheme that prevents it, and the code that enforces it.

6 min readUpdated August 19, 2026Attribution

Naming conventions fail for a boring reason: they live in a document nobody opens. The only convention that survives is one enforced in code, where a wrong value cannot be typed in the first place.

The rules

  • Lowercase, always. Most tools treat Spring_Sale and spring_sale as different campaigns and will silently split your numbers.
  • Underscores, never hyphens or spaces. Pick one separator and never revisit it. Hyphens read as minus signs in some spreadsheet tools.
  • No dates in campaign names. spring_sale with a date range in the report beats spring_sale_march_2026, which fragments year over year.
  • `{trigger}_{intent}`. What caused the send, then what you want them to do. booking_confirm, invoice_due, review_request.
  • Never put the vendor in `utm_source`. Use imessage. You may switch providers; the channel does not change.

Enforce it in the type system

A union type turns a naming convention from a suggestion into a compile error, which is the only form of documentation that works.

lib/campaigns.ts
// The full set of campaigns, in one place. Adding one is a deliberate act.
export const CAMPAIGNS = [
"booking_confirm",
"appointment_reminder",
"appointment_nudge",
"review_request",
"invoice_due",
"winback_offer",
"gap_alert",
"speed_to_lead",
] as const;
 
export type Campaign = (typeof CAMPAIGNS)[number];
 
// tagged() now rejects a typo at build time rather than at reporting time.
export function tagged(url: string, tags: { campaign: Campaign; content?: string }) {
const u = new URL(url);
u.searchParams.set("utm_source", "imessage");
u.searchParams.set("utm_medium", "sms");
u.searchParams.set("utm_campaign", tags.campaign);
if (tags.content) u.searchParams.set("utm_content", norm(tags.content));
return u.toString();
}

The union type is the documentation

Autocomplete shows the whole list at the call site. Nobody has to remember the convention, or find the wiki page, because the editor offers the correct values and refuses the wrong ones.

For the people who are not in the codebase

Front-desk staff sending from a provider inbox cannot import a type. Give them the UTM builder — it normalises to lowercase with underscores automatically, so the output is consistent regardless of what gets typed in.

Auditing what you already have

Export your campaign list, lowercase everything, and look for near-duplicates. Anything within an edit distance of two is almost certainly the same campaign recorded twice. Consolidate, then put the type in place so it cannot happen again.

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.