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

# Analytics

> Cross-channel delivery, engagement, and cost analytics via the Orbit Analytics API

# Analytics

Orbit's Analytics endpoints expose the same delivery, engagement, and cost datasets that power the dashboard's Insights page. Use them to embed Orbit metrics in your internal BI tooling, schedule exports to a data warehouse, or programmatically alert on delivery / spend regressions.

All analytics endpoints live under `/api/v1/analytics`, require an API key with the `analytics:read` scope, and are aggregated against a read replica so heavy reporting queries never contend with the live send path. Each endpoint is rate-limited at 60 requests / minute per tenant.

Every response uses the standard Orbit envelope: the payload is under `data`, and `meta` carries the `request_id` + `timestamp`.

## Query delivery metrics

Aggregate sent / delivered / failed / read counts with a time-series breakdown. Filter by `channel`, `status`, or `campaign_id`, and bucket the series with `group_by` (`hour`, `day`, `week`, `month` — defaults to `day`).

```bash theme={null}
curl -G https://api.orbit.devotel.io/api/v1/analytics/messages \
  -H "X-API-Key: dv_live_sk_..." \
  --data-urlencode "channel=sms" \
  --data-urlencode "status=delivered" \
  --data-urlencode "start_date=2026-05-01T00:00:00Z" \
  --data-urlencode "end_date=2026-05-08T00:00:00Z" \
  --data-urlencode "group_by=day"
```

### Response

```json theme={null}
{
  "data": {
    "totals": {
      "total_sent": 39534,
      "total_delivered": 38901,
      "total_failed": 633,
      "total_read": 21044,
      "delivery_rate": 98.4,
      "read_rate": 54.1,
      "avg_delivery_time_ms": 1840
    },
    "time_series": [
      { "period": "2026-05-01", "total_sent": 12345, "total_delivered": 12180, "total_failed": 165, "total_read": 6500, "delivery_rate": 98.66, "read_rate": 53.36, "avg_delivery_time_ms": 1790 },
      { "period": "2026-05-02", "total_sent": 13987, "total_delivered": 13760, "total_failed": 227, "total_read": 7420, "delivery_rate": 98.38, "read_rate": 53.91, "avg_delivery_time_ms": 1875 },
      { "period": "2026-05-03", "total_sent": 13202, "total_delivered": 12961, "total_failed": 241, "total_read": 7124, "delivery_rate": 98.17, "read_rate": 54.95, "avg_delivery_time_ms": 1862 }
    ],
    "group_by": "day"
  },
  "meta": {
    "request_id": "req_xyz789",
    "timestamp": "2026-05-09T00:00:00Z"
  }
}
```

## Query parameters

These filters are shared across the metric and cost endpoints (each endpoint accepts the subset documented below).

| Parameter                 | Description                                                                                                                                          |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `start_date` / `end_date` | ISO 8601 timestamps **with offset** (e.g. `2026-05-01T00:00:00Z`). Both default to the last 30 days when omitted. The range may not exceed 365 days. |
| `days`                    | Rolling lookback window, in days, as an alternative to `start_date` / `end_date` (e.g. `7`, `30`, `90`, `365`). Capped at 365.                       |
| `channel`                 | One of `sms`, `whatsapp`, `rcs`, `email`, `viber`, `voice`, `push`, `messenger`, `telegram`.                                                         |
| `status`                  | (`/messages` only) One of `queued`, `sent`, `delivered`, `failed`, `read`, `undelivered`.                                                            |
| `campaign_id`             | (`/messages` only) Restrict the rollup to a single campaign.                                                                                         |
| `group_by`                | (`/messages`, `/costs`) Time bucket for the series: `hour`, `day`, `week`, or `month`. Defaults to `day`.                                            |

## Endpoints

| Endpoint                                       | Method     | Description                                                                             |
| ---------------------------------------------- | ---------- | --------------------------------------------------------------------------------------- |
| `/api/v1/analytics/messages`                   | GET        | Aggregate delivery metrics + time-series (the example above).                           |
| `/api/v1/analytics/messages/by-channel`        | GET        | Per-channel delivery breakdown (includes email open / click / complaint counts).        |
| `/api/v1/analytics/messages/by-country`        | GET        | Per-destination-country delivery breakdown.                                             |
| `/api/v1/analytics/messages/errors`            | GET        | Failed messages grouped by error code, with per-code share.                             |
| `/api/v1/analytics/costs`                      | GET        | Spend totals, per-currency + per-channel breakdown, and a cost time-series.             |
| `/api/v1/analytics/funnel/{step}/drop-reasons` | GET        | Top carrier rejection codes for a funnel step (see below).                              |
| `/api/v1/analytics/telegram`                   | GET        | Telegram-specific KPIs (bot-blocked rate, group-vs-DM split, custom-emoji rate).        |
| `/api/v1/analytics/deliverability`             | GET        | Per-channel deliverability dashboard.                                                   |
| `/api/v1/analytics/scheduled-reports`          | GET / POST | List + schedule recurring report exports (see [Scheduled exports](#scheduled-exports)). |

## Cost analytics

```bash theme={null}
curl -G https://api.orbit.devotel.io/api/v1/analytics/costs \
  -H "X-API-Key: dv_live_sk_..." \
  --data-urlencode "channel=sms" \
  --data-urlencode "start_date=2026-05-01T00:00:00Z" \
  --data-urlencode "end_date=2026-05-08T00:00:00Z" \
  --data-urlencode "group_by=day"
```

### Response

```json theme={null}
{
  "data": {
    "totals": {
      "total_messages": 39534,
      "total_spend": 287.91,
      "avg_cost_per_message": 0.0073,
      "total_segments": 41208,
      "by_currency": [
        { "currency": "USD", "total_messages": 39534, "total_spend": 287.91, "avg_cost_per_message": 0.0073, "total_segments": 41208 }
      ]
    },
    "time_series": [
      { "period": "2026-05-01", "currency": "USD", "total_messages": 12345, "total_spend": 89.92, "avg_cost_per_message": 0.0073, "total_segments": 12870 }
    ],
    "by_channel": [
      { "channel": "sms", "currency": "USD", "total_messages": 39534, "total_spend": 287.91, "avg_cost_per_message": 0.0073, "total_segments": 41208 }
    ],
    "group_by": "day"
  },
  "meta": {
    "request_id": "req_abc123",
    "timestamp": "2026-05-09T00:00:00Z"
  }
}
```

### Field reference

These cost fields count **billed** messages only — sends that actually incurred a charge. Free-tier, promotional, and zero-priced sends carry no charge and are excluded, so the counts here are lower than the send totals from `/analytics/messages`.

| Field                  | Definition                                                                                                                                                                                                                                                                                                                                           |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `total_messages`       | Number of billed messages in the group — sends that incurred a charge. Zero-cost and unpriced sends are excluded, so this is smaller than your total send count.                                                                                                                                                                                     |
| `total_spend`          | Sum of the platform charges for those billed messages, in the group's `currency`.                                                                                                                                                                                                                                                                    |
| `avg_cost_per_message` | `total_spend` divided by `total_messages` — the average charge **per billed message**, rounded to four decimal places. Because zero-cost sends are excluded from the denominator, this reads higher than dividing `total_spend` by every message you sent. It is a per-message figure, not per-segment. A group with no billed messages returns `0`. |
| `total_segments`       | Total SMS segments across those messages. A long SMS is split into multiple segments, so `total_segments` is at or above `total_messages`; use it to reconcile per-segment carrier charges separately from the per-message average.                                                                                                                  |

Every row is grouped by `currency`, so a tenant that changed billing currency mid-range gets one row per currency rather than a mixed sum. To reconcile a total against `avg_cost_per_message`, divide `total_spend` by `total_messages` (billed) for the matching currency — not by your overall send count.

## Funnel drop-off reasons

Drill into *why* messages dropped at a funnel step. `{step}` is one of `sent`, `delivered`, `read`, or `queued`; the response returns the top carrier rejection codes (default 5, max 20 via `limit`) enriched with human-readable names and categories from Orbit's carrier-error dictionary.

```bash theme={null}
curl -G https://api.orbit.devotel.io/api/v1/analytics/funnel/sent/drop-reasons \
  -H "X-API-Key: dv_live_sk_..." \
  --data-urlencode "channel=sms" \
  --data-urlencode "start_date=2026-05-01T00:00:00Z" \
  --data-urlencode "end_date=2026-05-08T00:00:00Z" \
  --data-urlencode "limit=5"
```

### Response

```json theme={null}
{
  "data": {
    "step": "sent",
    "reasons": [
      {
        "error_code": "30007",
        "name": "Carrier filtered",
        "description": "The carrier blocked the message as suspected spam.",
        "category": "carrier_filtering",
        "count": 318,
        "percentage": 50.2
      },
      {
        "error_code": "30005",
        "name": "Unknown destination",
        "description": "The destination handset is unknown or has been deactivated.",
        "category": "invalid_destination",
        "count": 142,
        "percentage": 22.4
      }
    ]
  },
  "meta": {
    "request_id": "req_def456",
    "timestamp": "2026-05-09T00:00:00Z"
  }
}
```

## Common errors

| Code               | HTTP | Cause                                                                                                                                                                                                           | Fix                                                                                            |
| ------------------ | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `VALIDATION_ERROR` | 422  | A query parameter failed validation — an unknown `channel` / `status` / `group_by`, a malformed timestamp, `start_date` not earlier than `end_date`, or a range / `days` value beyond the 365-day lookback cap. | Inspect `error.details.issues` for the offending field; narrow the range or correct the value. |
| `RATE_LIMITED`     | 429  | Per-tenant rate limit exceeded (60 req/min on analytics endpoints).                                                                                                                                             | Honour `Retry-After` and back off.                                                             |

## Scheduled exports

For ongoing exports, prefer scheduled reports over polling the metric endpoints. `POST /api/v1/analytics/scheduled-reports` registers a recurring report that Orbit renders and emails to your configured recipients on a daily / weekly cadence; `GET /api/v1/analytics/scheduled-reports` lists the active schedules. Creating or editing a schedule requires an `owner` or `admin` role because the report payload includes per-tenant PII (top contacts, spend, etc.).
