Skip to content
imessageapi

Tracking iMessage campaigns in PostHog end to end

Tagged links get the visit into your analytics. Getting from a visit to “this campaign earned $1,521” takes four more steps, and this is all of them.

9 min readUpdated August 22, 2026Attribution

This site runs on PostHog, so this walkthrough uses it — but the shape is identical in any product-analytics tool. The pattern is always: capture the UTMs on landing, persist them for the session, attach them to the conversion, then report on the conversion rather than the visit.

Step 1 — Let the tool capture the UTMs

PostHog reads UTM parameters off the landing URL automatically and stores them as properties on the pageview. You do not need to parse anything yourself. What you do need is for the parameters to actually be there — which is what tagging every link is for.

instrumentation-client.ts
import posthog from "posthog-js";
 
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN!, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
defaults: "2026-05-30",
// Profiles for anonymous visitors too, so a first-touch text click
// still connects to a conversion that happens on a later visit.
person_profiles: "always",
});

Step 2 — Persist first touch

The problem with messaging is the gap. Someone taps your link on Tuesday, looks around, and books on Thursday from a search. Without persistence, Thursday gets the credit and your text campaign looks like it did nothing.

lib/attribution.ts
const KEY = "first_touch";
 
export function captureFirstTouch() {
if (typeof window === "undefined") return;
 
try {
if (localStorage.getItem(KEY)) return; // first touch wins
 
const params = new URLSearchParams(window.location.search);
const campaign = params.get("utm_campaign");
if (!campaign) return;
 
localStorage.setItem(KEY, JSON.stringify({
campaign,
source: params.get("utm_source"),
medium: params.get("utm_medium"),
content: params.get("utm_content"),
at: new Date().toISOString(),
}));
} catch {
// Private browsing throws. Attribution is nice to have, not load-bearing.
}
}

Decide first touch or last touch, and write it down

Both are defensible. What is not defensible is different reports using different rules, which is what happens when nobody decides. For messaging, first touch is usually more honest — the text is what started it.

Step 3 — Fire an event on the thing that pays you

Not the landing-page view. The booking, the payment, the confirmed appointment. Attach the campaign and, critically, a value — a conversion without a number cannot be turned into a revenue report.

on-booking.ts
import posthog from "posthog-js";
import { firstTouch } from "@/lib/attribution";
 
export function trackBooking(booking: Booking) {
const touch = firstTouch();
 
posthog.capture("booking_completed", {
value: booking.totalCents / 100,
service: booking.service,
utm_campaign: touch?.campaign ?? "none",
utm_content: touch?.content ?? null,
days_since_touch: touch ? daysSince(touch.at) : null,
});
}

Step 4 — Tag the send side too

Analytics only sees people who tapped. To compute a conversion rate you need the denominator — how many you sent. Emit a server-side event when a campaign goes out, with the same campaign name, and the two halves join up.

server-side send event
// Same utm_campaign value on both sides is what makes this joinable.
await posthog.capture({
distinctId: customer.id,
event: "message_sent",
properties: {
utm_campaign: "appointment_reminder",
channel: result.deliveredAs, // "imessage" | "sms" | "rcs"
template: "reminder24h",
},
});

Step 5 — Build the one report that matters

A funnel from message_sent to booking_completed, broken down by utm_campaign, with the value property summed. That single view answers what each campaign cost and what it returned, which is the only question worth asking. Then add a holdout so you are measuring your effect rather than your customers' existing habits.

Break down by delivered channel too

Because you recorded whether each message went out as iMessage, RCS or SMS, you can compare conversion rates across bubble colours on identical copy. That is a genuinely interesting number, and almost nobody measures it.

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.