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

# Usage & delivery anomaly alert rules

> Define org-scoped alert rules over your SMS delivery rate, outbound message volume, and spend — evaluated against a rolling window or a learned baseline, surfaced in your dashboard notifications.

# Usage & delivery anomaly alert rules

Define org-scoped alert rules that watch the signals that move a CPaaS account: **SMS delivery rate**, **outbound message volume**, and **spend**. When the metric breaks your threshold — or deviates sharply from its own learned baseline — the rule fires an in-app notification the whole team sees in the dashboard's Notification Center.

Rules are **read-only** over your account. Evaluation aggregates your already-persisted message and billing-ledger rows; the only writes are alert-rule state and the notification it fires. No outbound traffic ever results from a rule.

This complements [In-dashboard notifications and the Notification Center](/guides/in-dashboard-notifications) (the surface rules notify on) and [Per-API-key usage budgets and threshold alerts](/guides/api-key-usage-budget-alerts) (a browser-local tripwire; everything on this page is org-scoped and shared with your team).

## Endpoints

All endpoints share the standard `{ "data": ..., "meta": ... }` envelope and are tenant-scoped.

| Method   | Path                          | Purpose                                                                                                  |
| -------- | ----------------------------- | -------------------------------------------------------------------------------------------------------- |
| `GET`    | `/usage/alert-rules`          | List your rules with evaluator state (`last_status`, `last_value`, `last_evaluated_at`, `last_fired_at`) |
| `POST`   | `/usage/alert-rules`          | Create a rule (returns `201`)                                                                            |
| `PATCH`  | `/usage/alert-rules/{id}`     | Update name, comparator, threshold, window, cooldown, or `enabled`                                       |
| `DELETE` | `/usage/alert-rules/{id}`     | Delete a rule                                                                                            |
| `GET`    | `/usage/alert-rules/events`   | Fired-alert feed, newest first, capped at the last 50 events                                             |
| `POST`   | `/usage/alert-rules/evaluate` | Evaluate all of this tenant's rules now (returns `{"tenantId", "rulesEvaluated", "breached", "fired"}`)  |

A tenant can hold up to 50 rules.

## Auth requirements

* **Reads** (`GET ...`, `GET .../events`) — a valid API key/JWT whose scopes include `usage:read`.
* **Writes and evaluate-now** (`POST`, `PATCH`, `DELETE`, `POST .../evaluate`) — additionally require an org role of `owner`, `admin`, or `developer`. The audit log records create/update/delete with the acting user.

## Rule shapes

Each rule targets one metric, using one of two modes.

### Metrics

| `metric`                  | Label                   | Unit                  | Direction the rule guards                   |
| ------------------------- | ----------------------- | --------------------- | ------------------------------------------- |
| `sms_delivery_rate`       | SMS delivery rate       | percent (e.g. `95.7`) | Guards a **drop** (routing/carrier trouble) |
| `outbound_message_volume` | Outbound message volume | count                 | Guards a **surge** (pumping misuse)         |
| `spend`                   | Spend                   | USD                   | Guards a **surge** (bill shock)             |

Delivery rate and volume are computed over your outbound messages with the same status filters the dashboard's delivery KPI card uses, and spend is aggregated from wallet-ledger debits excluding admin adjustments — so an alert fires on exactly the numbers the dashboard shows.

### Threshold mode (default)

Compares the metric's value over a trailing window against a fixed number.

* `comparator`: one of `gt`, `gte`, `lt`, `lte`
  * `lt` / `lte` — "must stay above X" (e.g. delivery rate below 95%)
  * `gt` / `gte` — "must stay below X" (e.g. volume above 10k/day, spend above \$100)
* `threshold`: a finite number on the metric's native unit
* `window_days`: the trailing window the threshold aggregates over, 1–30 (default 7)

A threshold rule **cannot** exist without both a comparator and a threshold — create and update both reject it (`422`).

### Anomaly mode

Compares the newest complete UTC day against a learned 28-day baseline using a z-score classifier. You set no threshold — "unusual for your account" is the trigger. For delivery rate it detects a downward drop; for volume and spend, an upward spike (the metric on volume/spend rules is inverted internally so a spike registers the same way).

Anomaly rules must not carry `comparator`/`threshold` at create time (they are ignored); pass only `mode: "anomaly"` plus `window_days`, `cooldown_hours`, `notify_channels`, `enabled`.

## Field reference (create / update body)

| Field             | Type    | Constraints                    | Default                    |
| ----------------- | ------- | ------------------------------ | -------------------------- |
| `name`            | string  | 1–160 chars                    | required                   |
| `metric`          | enum    | one of the three metrics above | required                   |
| `mode`            | enum    | `threshold`, `anomaly`, `any`  | `threshold`                |
| `comparator`      | enum    | `gt`, `gte`, `lt`, `lte`       | required in threshold mode |
| `threshold`       | number  | finite                         | required in threshold mode |
| `window_days`     | integer | 1–30                           | 7                          |
| `notify_channels` | array   | subset of `["in_app"]`         | `["in_app"]`               |
| `cooldown_hours`  | integer | 1–720                          | 24                         |
| `enabled`         | boolean |                                | `true`                     |

<Warning>
  **`notify_channels` currently accepts only `in_app`.** Alerts surface through the existing dashboard notification channel (the bell / Notification Center); email or webhook fan-out is not yet on this surface.
</Warning>

## The fired event lifecycle

A scheduler-driven sweep continuously evaluates every tenant that has at least one rule, and `POST /usage/alert-rules/evaluate` runs the same evaluation on demand for your tenant. On every evaluation tick, each enabled rule updates its runtime fields (`last_evaluated_at`, `last_status`, `last_value`) — even in a breach.

An event is appended to your fired-alert feed **(max 50, newest first)** and a notification pushed **only when**:

* the rule's status transitions **into** `breached`, or
* it stays in breach and the `cooldown_hours` re-notify window (default 24h) has elapsed since the last notification.

While the rule stays in breach but inside the cooldown, state still updates but no new event/notification is emitted — the same signal does not re-page you for 24 hours.

A metric that cannot be resolved (no data in the window, or a provisioning gap) is reported as `insufficient_data` and **never fires a false alert**.

### Event payload

Each entry in `GET /usage/alert-rules/events`:

| Field                             | Meaning                                                                                           |
| --------------------------------- | ------------------------------------------------------------------------------------------------- |
| `id`                              | Unique event id                                                                                   |
| `rule_id`, `rule_name`            | Which rule fired                                                                                  |
| `metric`                          | The rule's metric                                                                                 |
| `status`                          | `breached` (cleared states are not events)                                                        |
| `value`                           | Metric value at fire time (`null` when insufficient data)                                         |
| `threshold`, `comparator`, `mode` | The rule's configured trigger at fire time                                                        |
| `message`                         | Human-readable sentence ("SMS delivery rate is 82.4%, below the 95% threshold over the last 7d.") |
| `deep_link`                       | Dashboard path to the metric surface (currently `/insights`)                                      |
| `fired_at`                        | ISO-8601 timestamp                                                                                |

Poll `GET /usage/alert-rules/events` to consume alerts from an API integration, or open the Notification Center in the dashboard.

## Where alerts land in the dashboard

A firing rule reuses the in-app notification channel: it appears in the bell dropdown and the Notification Center with severity **warning**, titled `Usage alert: <your rule name>`, with the threshold/anomaly sentence as the body and a **deep link to the Insights page**. See [In-dashboard notifications and the Notification Center](/guides/in-dashboard-notifications) for triage/read/dismiss semantics.

## Example: delivery-failure-rate alert

Create a rule that fires when SMS delivery rate over the trailing week falls below 95%, and re-fires at most once a day while it stays breached:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/usage/alert-rules \
  -H "Authorization: Bearer dv_live_sk_…" \
  -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
  }'
```

Response (`201`):

```json theme={null}
{
  "data": {
    "id": "b2e2a9a1-…",
    "name": "SMS delivery rate below 95%",
    "metric": "sms_delivery_rate",
    "metric_label": "SMS delivery rate",
    "unit": "percent",
    "direction": "higher_is_better",
    "metric_path": "/insights",
    "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,
    "created_at": "2026-08-27T12:00:00.000Z",
    "updated_at": "2026-08-27T12:00:00.000Z",
    "created_by_user_id": "user_…"
  },
  "meta": { "request_id": "req_…", "timestamp": "2026-08-27T12:00:00.000Z" }
}
```

Evaluate now (optional — the scheduler sweep also evaluates it on its own cadence):

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/usage/alert-rules/evaluate \
  -H "Authorization: Bearer dv_live_sk_…"
```

```json theme={null}
{ "data": { "tenantId": "acme", "rulesEvaluated": 1, "breached": 1, "fired": 1 }, "meta": { … } }
```

Then read the fired feed:

```bash theme={null}
curl https://api.orbit.devotel.io/api/v1/usage/alert-rules/events \
  -H "Authorization: Bearer dv_live_sk_…"
```

```json theme={null}
{
  "data": {
    "items": [
      {
        "id": "evt_…",
        "rule_id": "b2e2a9a1-…",
        "rule_name": "SMS delivery rate below 95%",
        "metric": "sms_delivery_rate",
        "status": "breached",
        "value": 82.4,
        "threshold": 95,
        "comparator": "lt",
        "mode": "threshold",
        "message": "SMS delivery rate is 82.4%, below the 95% threshold over the last 7d.",
        "deep_link": "/insights",
        "fired_at": "2026-08-27T12:00:00.000Z"
      }
    ],
    "total": 1
  },
  "meta": { … }
}
```

A mode: "anomaly" variant — pick up when the newest day's delivery rate deviates sharply from the learned 28-day baseline, without fixing a threshold:

```json theme={null}
{ "name": "Delivery-rate anomaly", "metric": "sms_delivery_rate", "mode": "anomaly" }
```

## Troubleshooting

**`422` says "comparator is required for a threshold rule".**
Pass both `comparator` and `threshold` whenever the rule is threshold mode (create, and any update on a threshold rule; `mode` defaults to `threshold`).

**A rule's `last_status` reads `insufficient_data`.**
The metric could not be resolved in the window — usually zero outbound messages (or wallet debits, for spend rules) in the window. Evaluation reports no value rather than firing on a fabricated 0%.

**The rule fires fewer notifications than expected.**
That is cooldown semantics: while a rule stays in breach, it re-notifies only once per `cooldown_hours` (default 24h); new breach transitions always notify immediately.

**You want email or webhook instead of the dashboard notification.**
The only configured channel today is the dashboard notification (`notify_channels: ["in_app"]`). Poll the fired-event feed to integrate alerts into your own systems.
