SMS click-through parity
A channel-level click-through surface for SMS: for each campaign (or messaging queue), how many outbound sends went out, how many carried a tracked short link, how many got clicked, and the resulting CTR. Per-link statistics answer “who clicked this link”; this endpoint answers the channel question — “what is the click-through rate on this campaign.” Base path:/api/v1/analytics
Dashboard: the same aggregation powers Insights → SMS click-throughs in the dashboard.
What the parity surface measures
Verifying a click figure means reconciling two sides over the same window:- Sends side — outbound SMS rows for your tenant, grouped by the attribution key you pick (
campaignorqueue). - Click side — every shortened link embedded in an outbound SMS, joined back to its message, rolled up per the same key.
window and group. A bucket where the attribution key never resolved (ad-hoc single-message sends carry no campaign id) reports as unattributed instead of disappearing.
Endpoint
GET /api/v1/analytics/sms-click-through
Response shape
The standard envelope wraps the payload:{ "data": { … }, "meta": { "request_id": "…", "timestamp": "…" } }. The data payload:
Rate fields are
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.
Responses are cached for 60 seconds behind a stampede lock; repeated polling within a minute returns the same payload.
Worked example — group by queue, 24-hour window
Flipgroup to queue to attribute clicks to the messaging service (queue) recorded at send time, with the provider tag as the fallback key — the same precedence the dashboard toggle uses. Buckets where the queue never resolved land under the unattributed row rather than disappearing.
200:
TX Alerts queue sends transactional SMS with no shortened links, so its rate fields are null and its clicks zero — the bucket still counts toward totals.sends. Page 2 of this response returns the remaining bucket (pagination.total is 3, totalPages is 2) — including null bucketKey rows for sends that never resolved a queue. See the Analytics API page for a pagination loop that drains every page.
Interpretation
- CTR disputes against delivery reports. A delivery status tells you the message reached the handset; it says nothing about engagement. Compare
verified_click_rate(human clicks) againstclick_rate(any click) — a large gap means scanners, link previews, or bots are inflating the raw count, and the verified rate is the defensible figure. - Untracked sends.
sendsminustracked_sendsis the volume sent without a shortened link; those messages can never register a click, and the rate denominator excludes them by design. - Double counting. A send carrying several links, or one recipient clicking several times, lands in
total_clicks. The send-level counters (tracked_sends,sends_with_clicks) count each message once, so the fan-out never inflates the denominator.
Relationship to campaign analytics
Usegroup=campaign as the click-through axis alongside your campaign-level delivery and cost dashboards. For A/B tests, verified_click_rate is the winner metric — it filters bot traffic that otherwise skews small-sample comparisons. ROAS surfaces attribute revenue; this surface attributes engagement, and a healthy ROAS with a falling CTR usually means the link placement or copy, not the offer, needs work.
See also
- Analytics API — message traffic, deliverability, and scheduled reports
- Short Links API — per-link creation, stats, and tenant-wide link insights