Reading the SMS click-through parity panel
The SMS click-throughs surface answers the channel-level question the per-link analytics can’t: “what is the click-through rate on this campaign (or queue)?” Per-link stats under Insights → Links tell you who clicked a given short URL; the parity panel rolls those rows up per campaign or messaging queue so SMS click-through can be compared side by side with the email channel’s Clicked metric. Everything the panel shows is a read-only rollup over the same shorten_urls pipeline that mints tracked short links on send (metadata.shorten_urls or the tenant SMS/WhatsApp auto-shorten settings) — so the panel can never disagree with the per-link drill-down. See the short links guide for how links get minted and tracked, and the SMS click-through API reference for the underlying endpoint.
1. What the panel reports
For each campaign or queue in the window, the panel reports:- Sends — outbound SMS rows in the window, whether or not they carried a tracked link.
- Tracked — sends that carried at least one tracked short link.
- Clicked — sends whose tracked links recorded at least one click.
- Clicks — raw clicks across the bucket’s links, including bot and link-preview traffic.
- Verified — clicks that passed the verified-click predicate (bot and link-preview scanners filtered out; see the short links guide for how filtering works).
- CTR — clicked sends divided by tracked sends, in the
[0, 1]range. If a bucket has no tracked sends, CTR shows as—rather than a misleading zero. - Verified CTR — verified clicks divided by tracked sends. Use this when comparing against email’s bot-filtered Clicked metric.
Reconciliation math — how Verified CTR is derived
Every row reconciles two sides over the same window, and you should be able to reproduce the panel from the raw surfaces:- Sends side — outbound SMS rows for your tenant, grouped by the attribution key (
campaignorqueue). This is the denominator. - Click side — every shortened link embedded in an outbound SMS, joined back to its message, rolled up per the same key. This is the numerator family.
quality_score >= 0.5 (user-agent present, not a known bot or preview-fetch signature). Raw clicks stay in the store, so the filter lives at the analytics read layer and retro-applies to already-collected data. See short links and click tracking for the scoring rules and why a missing user-agent scores below the verification bar.
Because the rates divide by tracked_sends rather than sends, a bucket where most messages carried no link (transactional traffic, one-off operator sends) shows a small denominator and a high CTR — that’s correct, not inflated. Watch untracked volume instead: it’s the volume that structurally cannot register engagement, and the only fixes for it are upstream — shorten more of what you send (see §2).
2. How tracked links get minted at send
A send contributes to theTracked column only if it carried at least one tracked short link. Three switches control minting, in descending precedence:
metadata.shorten_urlson the send payload — explicit per-send opt-in.POST /api/v1/messages/smswith"metadata": { "shorten_urls": true }rewrites everyhttp(s)://URL in the body into a tracked short link before the message leaves, stampingcampaign_idandmessage_idfrom the send onto each minted link so clicks attribute back to this send.- Tenant SMS auto-shorten (
sms_auto_shorten_urls, default ON) — when the per-send flag is absent, this setting still rewrites URLs longer than 30 characters, but only when the minted short URL is strictly shorter than the original. On SMS the point of shortening is to cut segment cost, so an equal-or-longer swap is refused and the URL ships verbatim. - Tenant WhatsApp auto-track (
whatsapp_auto_shorten_urls, default ON) — the WhatsApp counterpart, which also mints for scheme-lesswww.spans (anchored onwww.plus at least two dot-separated labels, so prose likeindex.jsis never mistaken for a link). Without it, a tap on a raw link in a free-text WhatsApp body never reaches the redirect and no click event fires.
Tracked count, never as a failed message. For the mint mechanics (net-shortening guard, failure semantics, the on-wire swap), see the short links cookbook, recipe 2.
3. Fixing the unattributed bucket
The unattributed bucket collects sends where the attribution key never resolved — no campaign_id (when grouped by campaign) or no resolvable queue (when grouped by queue). It is not an error grouping; it is the catch-all for one-off sends, compose-dialog sends, and older payloads. A large unattributed share of sends means attribution is being lost upstream, and the fix is to stamp the key at send time.
Stamp campaign_id on the outbound payload. The outbound templates path is the natural place — templates are authored once and reused in campaigns, so a template picked into a campaign carries the campaign id through every send that renders it. See the outbound templates guide for template lifecycle and how a template enters a campaign.
For a direct API send, stamp it on the payload itself:
cURL
cmp_may26 bucket on the campaign grouping, its minted link carries the same campaign_id, and the recipient’s click attributes back to the campaign on both this panel and the per-campaign link rollup.
For the queue grouping, attribution follows the messaging service (queue) recorded at send time, with the provider tag as the fallback key — the same precedence the dashboard toggle uses. If your routing config never resolves a queue for a slice of sends, those sends land in unattributed on the queue grouping even when they carry a campaign_id. The fix there is routing config: resolve the queue on the messaging service so every outbound payload records one, and the bucket drains.
4. Comparing with the email channel’s Clicked metric
The parity claim is specific: Verified CTR on SMS compares with the Clicked metric on email campaign reports, because both are bot-filtered against a send denominator. Keep the semantics straight:- Email Clicked — recipients who clicked, over delivered email; the bot/preview filter (scanner user-agents, prefetch gateways) is applied before the number ships.
- SMS Verified CTR — verified clicks over tracked sends. The verified predicate (
quality_score >= 0.5) is the same class of filter, so the two channels’ “human engagement” numbers line up. - SMS raw CTR — counts preview fetches and scanners as clicks. Comparing raw SMS CTR to email Clicked is apples-to-oranges; email’s number is already filtered.
- Link placement. SMS clickers click the only link in the body; email clickers pick among several links and buttons. A high email Clicked with low SMS Verified CTR usually means the SMS link is buried or the offer is weaker in the SMS copy, not that the metrics are broken.
- Deliverability composition. Email excludes bounces from the denominator; SMS has no bounce equivalent, so sends that never reached a handset stay in the SMS denominator. Channels with different deliverability baselines should not match on raw rate even with identical creative.
- Preview traffic composition. Corporate carriers and security gateways inflate SMS raw clicks more than email’s. A large raw-to-verified gap on SMS with a small one on email is normal.
Tracked ratio (sends − tracked_sends, §1): SMS sends with no minted link cannot click, so a low tracked share mechanically depresses Verified CTR against email.
5. API surface
The panel is a thin client overGET /api/v1/analytics/sms-click-through, mounted on the analytics router. Query params: group (campaign | queue), window (24h | 7d | 30d), page, pageSize (default 25, max 100). The response returns rows, totals, and pagination, keyed under the { data, meta } envelope; roles owner/admin/developer/viewer can all read it, and it’s cached for 60 seconds server-side behind a stampede lock.
cURL
200:
null when the bucket carried no tracked sends — CTR is undefined there, and the API returns null rather than a misleading 0. When a bucket is unattributed, bucketKey is null and the dashboard renders it as unattributed. totals aggregates across all buckets in the window, not just the returned page, so the KPI header stays correct while you paginate.
For request and response schemas, see the SMS click-through API reference. For how links get minted and tracked in the first place, see the short links guide.
6. Alerting on a degraded CTR
A parity read answers “how does SMS compare to email right now”; alerting answers “tell me when click-through degrades without me watching the panel.” Two alert surfaces cover this:- Usage & delivery anomaly alert rules — the transport-side tripwires: SMS delivery rate, outbound volume, spend. A routing or carrier regression that should show up as falling CTR usually shows up here first as a delivery-rate drop or a volume spike, so wire these before you wire engagement rules.
- KPI alerts on business metrics — the customer-experience tripwires: CSAT, NPS, CES, LLM spend, AI containment. When a click-through regression is really a downstream symptom (a broken flow the links lead into, a degraded agent), the KPI rule on the experience metric fires on exactly the numbers the dashboard shows.