Skip to main content

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 (campaign or queue).
  • Click side — every shortened link embedded in an outbound SMS, joined back to its message, rolled up per the same key.
The public response carries both sides per bucket, so you can reconcile them in one read: sends on one axis, tracked sends and click rollups on the other, over the identical 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:
Per-bucket fields: 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

Flip group 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.
Response 200:
The 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) against click_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. sends minus tracked_sends is 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

Use group=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