> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# SMS click-through parity: per-campaign and per-queue CTR

> Channel-level SMS click-through reporting — sends, tracked links, clicks, and CTR per campaign or messaging queue, reconciled across the whole window.

# 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`

| Param      | Type                   | Default    | Notes                                                                                                                                                             |
| ---------- | ---------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `group`    | `campaign` \| `queue`  | `campaign` | Attribution bucket. `campaign` uses the campaign id on the message; `queue` uses the messaging-service id recorded at send time, falling back to the provider tag |
| `window`   | `24h` \| `7d` \| `30d` | `7d`       | Trailing window to aggregate over                                                                                                                                 |
| `page`     | integer ≥ 1            | `1`        | 1-indexed page                                                                                                                                                    |
| `pageSize` | integer 1–100          | `25`       | Buckets per page                                                                                                                                                  |

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/analytics/sms-click-through?group=campaign&window=7d" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

## Response shape

The standard envelope wraps the payload: `{ "data": { … }, "meta": { "request_id": "…", "timestamp": "…" } }`. The `data` payload:

```json theme={null}
{
  "channel": "sms",
  "group": "campaign",
  "window": "7d",
  "rows": [
    {
      "bucketKey": "cmp_may26",
      "bucketLabel": "May promo",
      "sends": 52840,
      "tracked_sends": 51200,
      "sends_with_clicks": 3098,
      "total_clicks": 4150,
      "unique_clickers": 2871,
      "verified_clicks": 3302,
      "click_rate": 0.0605,
      "verified_click_rate": 0.0645
    }
  ],
  "pagination": { "page": 1, "pageSize": 25, "total": 12, "totalPages": 1 },
  "totals": {
    "buckets": 12,
    "sends": 118420,
    "tracked_sends": 109300,
    "sends_with_clicks": 6410,
    "total_clicks": 8920,
    "unique_clickers": 5890,
    "verified_clicks": 6610,
    "click_rate": 0.0586,
    "verified_click_rate": 0.0604
  }
}
```

Per-bucket fields:

| Field                 | Meaning                                                                   |
| --------------------- | ------------------------------------------------------------------------- |
| `bucketKey`           | Resolved campaign id, queue id/provider, or `null` for unattributed sends |
| `bucketLabel`         | Friendly label when the campaign/queue resolves; `null` otherwise         |
| `sends`               | Outbound SMS in the window, whether or not they carried a tracked link    |
| `tracked_sends`       | Sends that carried at least one shortened, tracked link                   |
| `sends_with_clicks`   | Sends whose links were clicked at least once                              |
| `total_clicks`        | Raw clicks (includes bot and link-preview traffic)                        |
| `unique_clickers`     | Distinct clicker IP addresses                                             |
| `verified_clicks`     | Clicks that passed the bot/link-preview filter — the human count          |
| `click_rate`          | `sends_with_clicks ÷ tracked_sends`, 0–1, rounded to 4 places             |
| `verified_click_rate` | `verified_clicks ÷ tracked_sends`, 0–1                                    |

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.

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/analytics/sms-click-through?group=queue&window=24h&pageSize=2" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

```typescript theme={null}
const res = await orbit.request(
  "GET",
  "/analytics/sms-click-through?group=queue&window=24h&pageSize=2",
);
// res.data.rows — one bucket per queue; res.data.totals covers the whole window
```

Response `200`:

```json theme={null}
{
  "channel": "sms",
  "group": "queue",
  "window": "24h",
  "rows": [
    {
      "bucketKey": "q_marketing_us",
      "bucketLabel": "Marketing US",
      "sends": 24110,
      "tracked_sends": 23380,
      "sends_with_clicks": 1521,
      "total_clicks": 2094,
      "unique_clickers": 1402,
      "verified_clicks": 1633,
      "click_rate": 0.0651,
      "verified_click_rate": 0.0698
    },
    {
      "bucketKey": "q_alerts",
      "bucketLabel": "TX Alerts",
      "sends": 8904,
      "tracked_sends": 0,
      "sends_with_clicks": 0,
      "total_clicks": 0,
      "unique_clickers": 0,
      "verified_clicks": 0,
      "click_rate": null,
      "verified_click_rate": null
    }
  ],
  "pagination": { "page": 1, "pageSize": 2, "total": 3, "totalPages": 2 },
  "totals": {
    "buckets": 3,
    "sends": 34210,
    "tracked_sends": 24120,
    "sends_with_clicks": 1690,
    "total_clicks": 2315,
    "unique_clickers": 1551,
    "verified_clicks": 1812,
    "click_rate": 0.0701,
    "verified_click_rate": 0.0751
  }
}
```

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](/api-reference/analytics#paginate-buckets-with-the-node-sdk) 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](/api-reference/analytics) — message traffic, deliverability, and scheduled reports
* [Short Links API](/api-reference/links) — per-link creation, stats, and tenant-wide link insights
