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

# Worked usage samples

> Read the per-channel usage aggregate with cost attribution, page its window, scope the companion alert rules, and branch on validation and scope errors.

## Worked usage samples

The chain below covers **read the aggregate → page its window → stand up an alert rule → branch on the errors**. The aggregate (`GET /api/v1/messages/usage/records`) is the Twilio-parity surface that rolls every billable row up per channel and direction with cost attribution — the exact numbers the usage-series and usage-records dashboard pages render. The cursor-windowed row-level feed behind the same pages (since/until/channel/cursor) lives on the [usage records overlay](/api-reference/endpoints/usagerecords); this page adds the aggregate and its alerting sibling.

### 1. Read the per-channel usage aggregate

<Note>
  `GET /api/v1/messages/usage/records?days=30`
</Note>

Returns one record per billing category (`sms-inbound`, `sms-outbound`, `mms-inbound`, `mms-outbound`, `voice-inbound`, `voice-outbound`) over a trailing window. `days` (1–365, default 30) sizes the window; SMS counts by segment, MMS and voice by message. Currency stays in the group key, so a mid-window currency flip returns one record per currency bucket rather than a mixed sum. `count` and `price` are the cost-attribution pair — `price` is the debited amount in `currency`.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X GET "https://api.orbit.devotel.io/api/v1/messages/usage/records?days=30" \
    -H "X-API-Key: dv_live_sk_your_key_here"
  ```

  ```typescript Node.js — the escape hatch (usage is below the typed surface) theme={null}
  import { Orbit } from '@devotel-orbit/node'

  const orbit = new Orbit({ apiKey: process.env.ORBIT_API_KEY })

  const usage = await orbit.request(
    'GET',
    '/api/v1/messages/usage/records?days=30',
  )
  // usage.records — one entry per (category, channel, direction, currency)
  ```
</CodeGroup>

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "period_days": 30,
      "records": [
        {
          "category": "sms-outbound",
          "channel": "sms",
          "direction": "outbound",
          "count": 12844,
          "price": 106.61,
          "currency": "USD"
        },
        {
          "category": "sms-inbound",
          "channel": "sms",
          "direction": "inbound",
          "count": 3102,
          "price": 0,
          "currency": "USD"
        },
        {
          "category": "voice-outbound",
          "channel": "voice",
          "direction": "outbound",
          "count": 417,
          "price": 38.9,
          "currency": "USD"
        }
      ]
    },
    "meta": {
      "request_id": "req_5c2f0a1e",
      "timestamp": "2026-09-10T12:00:00.000Z"
    }
  }
  ```
</ResponseExample>

Add `country_splits=1` (also accepts `true` / `yes`) to extend each record with a `countries` array — a per-ISO-3166 alpha-2 breakdown of `country`, `count`, and `price` for that category, derived from the recipient's E.164. Pass a smaller or larger `days` to re-slice the window; there is no cursor here because the aggregate returns its full category set in one body. The row-level counterpart with `since`/`until`/`channel`/`cursor` pagination is documented on the [usage records page](/api-reference/endpoints/usagerecords).

### 2. Stand up an alert rule on the same signal

<Note>
  `POST /api/v1/usage/alert-rules`
</Note>

The self-serve alerting sibling on this page watches the two signals that move a usage integration: delivery (`sms_delivery_rate`) and volume/spend (`outbound_message_volume`, `spend`). Create at most 50 rules per organization; a threshold rule needs a comparator (`gt` / `gte` / `lt` / `lte`) and a finite `threshold` on the metric's native unit — percent, count, or USD.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.orbit.devotel.io/api/v1/usage/alert-rules" \
    -H "X-API-Key: dv_live_sk_your_key_here" \
    -H "Idempotency-Key: usage-alert-sms-delivery-floor" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "SMS delivery rate below 95%",
      "metric": "sms_delivery_rate",
      "mode": "threshold",
      "comparator": "lt",
      "threshold": 95,
      "window_days": 7,
      "cooldown_hours": 24
    }'
  ```

  ```typescript Node.js — the escape hatch (usage is below the typed surface) theme={null}
  const rule = await orbit.request('POST', '/api/v1/usage/alert-rules', {
    name: 'SMS delivery rate below 95%',
    metric: 'sms_delivery_rate',
    mode: 'threshold',
    comparator: 'lt',
    threshold: 95,
    window_days: 7,
    cooldown_hours: 24,
  })
  // rule.id — PATCH or DELETE it at /api/v1/usage/alert-rules/:id
  ```
</CodeGroup>

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "id": "uar_5f2c9d81",
      "name": "SMS delivery rate below 95%",
      "metric": "sms_delivery_rate",
      "metric_label": "SMS delivery rate",
      "unit": "percent",
      "direction": "higher_is_better",
      "mode": "threshold",
      "comparator": "lt",
      "threshold": 95,
      "window_days": 7,
      "notify_channels": ["in_app"],
      "enabled": true,
      "cooldown_hours": 24,
      "last_evaluated_at": null,
      "last_status": null,
      "last_value": null,
      "last_fired_at": null,
      "metric_path": "/insights",
      "created_at": "2026-09-10T12:00:00.000Z",
      "updated_at": "2026-09-10T12:00:00.000Z",
      "created_by_user_id": "user_2g8h1c4f"
    },
    "meta": {
      "request_id": "req_7d3a2b8c",
      "timestamp": "2026-09-10T12:00:05.000Z"
    }
  }
  ```
</ResponseExample>

Each fired breach lands in `GET /api/v1/usage/alert-rules/events` and the in-app notification dropdown. `POST /api/v1/usage/alert-rules/evaluate` runs the evaluator for your organization now instead of waiting for the next sweep.

### 3. Error branches

Two branches decide what callers do next on this surface.

**Validation — a malformed rule body.** `POST /api/v1/usage/alert-rules` validates the body before anything persists. A threshold rule missing its comparator or threshold, a metric outside the registry (`sms_delivery_rate`, `outbound_message_volume`, `spend`), or a `window_days` above 30 returns `422 VALIDATION_ERROR` naming the rejected field:

```json 422 theme={null}
{
  "error": {
    "code": "VALIDATION_ERROR",
    "status": 422,
    "message": "comparator is required for a threshold rule",
    "details": {
      "issues": [
        {
          "field": "comparator",
          "message": "comparator is required for a threshold rule"
        }
      ]
    }
  },
  "meta": {
    "request_id": "req_8f1c3d4b",
    "timestamp": "2026-09-10T12:00:06.000Z"
  }
}
```

**Scope — the key lacks the usage or messages scope.** Reads on this page are scope-gated: the usage aggregate under `messages` needs a key minted with `messages:read` (or `messages:write`), and the `/usage/*` reads need `usage:read`. A dashboard session without an accepted org role, or an API key without the scope, is rejected `403` before any query runs:

```json 403 theme={null}
{
  "error": {
    "code": "FORBIDDEN",
    "status": 403,
    "message": "API key is missing the required scope: usage:read"
  },
  "meta": {
    "request_id": "req_9a4e5f6d",
    "timestamp": "2026-09-10T12:00:07.000Z"
  }
}
```

Treat both as terminal: fix the rule body before resending a `422`, and re-mint the key with the missing scope or assign an accepted role before retrying a `403`. The envelope mechanics — `error.code` / `status` / `details`, the retry-vs-terminal decision table — live in the [error-handling guide](/guides/error-handling-examples); only the usage-specific triggers above belong here.
