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

# Metric alerts

> Rules that fire when a share of recent calls crosses a threshold, and whom they notify.

A metric alert rule says "tell me when X% of calls ...": when a share of an agent's recent calls crosses a threshold, Sempera records a trigger and notifies the people and systems the rule names. Every metric is computed from records Sempera already keeps; a metric Sempera does not record (cost, sentiment, audio quality) is not offered.

## Routes

| Route | Scope | Notes |
| - | - | - |
| `GET /v1/alerts` | `agents:read` | This workspace's rules. `limit` (1 to 200, default 50), `page` (default 1), `sort` (`name`, `status`, `last_saved`, `last_triggered`; default `last_saved`), `direction` (`asc`, `desc`; default `desc`), `agent_id` or `agentId` (rules that cover one agent, every-agent rules included). A `limit` or `page` out of range (`page` is at most 100000) is refused with `400 validation_failed`, not clamped |
| `GET /v1/alerts/metrics` | `agents:read` | What a rule may measure and where it may notify |
| `POST /v1/alerts/preview` | `agents:write` | Try a rule without saving it or notifying anyone. `hours` is 24 or 168 |
| `POST /v1/alerts` | `agents:write` | Create a rule. `201` |
| `GET /v1/alerts/{id}` | `agents:read` | One rule and its trigger log, newest first |
| `PATCH /v1/alerts/{id}` | `agents:write` | Change any field; the fields you leave out keep their values. Optional `expected_updated_at` makes the save conditional (see Editing together) |
| `DELETE /v1/alerts/{id}` | `agents:write` | Delete a rule and clear any alert it raised |
| `POST /v1/alerts/{id}/evaluate` | `agents:write` | Run one evaluation of this rule now, the same pass the scheduler runs |
| `POST /v1/alerts/preview` | `agents:write` | Replay an unsaved rule against past calls. `hours` is `24` (default) or `168`. Nothing is saved and nobody is notified |

Writes need the `owner` role. A rule may only name agents the credential can reach, and a rule that names an agent the credential cannot read is left out of its list. Responses use the compatibility envelope `{"data": ..., "errors": null}`; the list is `{"data": {"alerts": [...], "pagination": {"limit", "page", "total", "total_pages"}}}`. Refusals use the standard error envelope ([Errors and limits](https://docs.sempera.org/errors-and-limits.md)).

## Editing together

To avoid overwriting another owner's edit, send the rule's `updated_at` back unchanged as `expected_updated_at` (a time with a zone):

```http theme={null}
PATCH /v1/alerts/{id}
{"name": "Too many hand-offs", "expected_updated_at": "2026-10-31T15:00:00+00:00"}
```

The rule is saved only if it still has that `updated_at`. The check and save are one step, so of two saves carrying the same value exactly one is applied. Enabling or disabling a rule (`enabled`) takes the same guard.

If another owner saved first, the answer is `409 conflict` with `reason: "stale_alert"` and `updated_at`, the value now on file. Nothing is written or audited, and no firing state is cleared. Show the owner what changed, then either discard the edit or explicitly retry with `expected_updated_at` set to that newer `updated_at`. A value that is not a time with a zone (including null) is `400 validation_failed` with `field: "expected_updated_at"`. A rule of another workspace, or one deleted meanwhile, is `404 not_found` and names no `updated_at`.

Without the field a save behaves as before: the last save wins.

## A rule

```http theme={null}
POST /v1/alerts
{"name": "Too many hand-offs",
 "metric": "handoffs",
 "thresholds": [{"percentage": 20, "severity": "warning"}, {"percentage": 40, "severity": "critical"}],
 "min_call_volume": 10,
 "lookback_minutes": 60,
 "agent_ids": "all",
 "notifications": [{"channel": "email", "to": "owner@example.com"}]}
```

| Field | Rule |
| - | - |
| `name` | Required, up to 200 characters |
| `metric` | One of the keys `GET /v1/alerts/metrics` lists: `failed_calls`, `slow_replies`, `handoffs`, `unfinished_requests`, `judge_failures`, `long_calls` |
| `cutoff` | Seconds, only for `slow_replies` (0.5 to 30, default 2) and `long_calls` (30 to 7200, default 300) |
| `thresholds` | 1 to 3 of `{"percentage", "severity"}`. Percentage is more than 0 and at most 100; severity is `info`, `warning` or `critical`. No two share a percentage or a severity, and a higher percentage carries a higher severity |
| `min_call_volume` | 1 to 100000. Below it nothing fires: one failure in one call is not a 100% failure rate |
| `lookback_minutes` | `10`, `30`, `60`, `180` or `1440` |
| `agent_ids` | `"all"` (default) or a non-empty list of agent ids |
| `notifications` | Up to 10 of `{"channel": "email", "to": "<address>"}` or `{"channel": "webhook", "to": "<webhook endpoint id>"}`. A webhook target is one of this workspace's own enabled endpoints ([Webhooks](https://docs.sempera.org/webhooks.md)), never a bare URL |
| `tools` | At most one of `{"id", "input"}`: `id` is an `api_request` tool's `tool_id` as `GET /v1/tools` shows it; `input` is up to 20 named values, each up to 2000 characters, which may reference the alert's fields as `{{name}}` (`GET /v1/alerts/metrics` lists them as `tool_variables`) |
| `enabled` | `true` (default) or `false` |

A rule read adds `id`, `object: "alert"`, `org_id`, `metric_label`, `category`, `severity` (the highest threshold's), `lookback_window` (the same number as `lookback_minutes`), `status` (`ok`, `triggered` or `disabled`), `firing_severity`, `last_saved` (also `updated_at`), `last_triggered`, `last_evaluated` and `created_at`. A detail read adds `triggers`, newest first and at most 100: each has `id`, `kind` (`fired`, `worsened` or `recovered`), `severity`, `value_pct`, `threshold_pct`, `call_volume`, `matched`, the window, `agent_ids`, `notified`, `notify_failed`, `notify_error` and `at`.

## How a rule fires

Every evaluation measures the metric over the lookback window for the agents in scope. When the call volume reaches the minimum and the share is at or above a threshold, the rule fires once, records a trigger, raises an alert in the Sempera App and notifies each target. It records a second trigger only when it worsens to a higher severity, and a recovery when an evaluation finds it clear; a recovery notifies the same targets with a "Cleared" message. Changing a rule's definition, or disabling it, clears its firing state.

Webhook targets receive the signed `metric_alert.triggered` event ([Webhooks](https://docs.sempera.org/webhooks.md)); this event is delivered only to the endpoints a rule names, never by subscription. A rule's tool runs through the same road, test mode and write rules as a tool step inside a call. A trigger's `notified` counts the targets that accepted delivery. A firing is also the `alert.fired` event your automation triggers can listen for (`GET /v1/automation/events`).

The App's Add notification menu offers Email and Webhook from the channel catalogue, plus the separate Tool slot. A webhook selects an existing enabled workspace endpoint, never a new URL. Ten email/webhook destinations do not consume the tool slot. Phone-call notifications are not available.

Fixture email records a local test message but does not count as delivery: `notified` stays zero for that target, with `notify_failed: true` and `notify_error: email_environment_fixture`. Provider acceptance counts as delivery, not proof that a person read it. Tool fixtures likewise do not count as delivery.

`POST /v1/alerts/{id}/evaluate` answers `{"evaluation", "alert", "triggers"}`. `evaluation.action` is `fired`, `worsened`, `recovered` or `none`, beside `value_pct`, `matched`, `call_volume`, `enough_volume`, `severity`, `threshold_pct`, `agent_ids` and the window. `DELETE` answers `{"id", "deleted": true}`.

`POST /v1/alerts/preview` takes the same body as `POST /v1/alerts`, checks it the same way, and replays it once a minute over the last `hours`. It saves no rule, records no trigger and sends nothing, so `notifications_sent` is always `0`. It reads only the agents both the rule and the credential can reach, and any other `hours` is `400 validation_failed`. It answers `{"hours", "from", "to", "triggered", "evaluations", "interval_minutes", "windows", "call_volume", "notifications_sent"}`: `triggered` counts each rise into a firing or higher severity, and `windows` holds the hourly measurements plus every change of severity.

The call metrics read preview and simulator conversation records, the same records Attention reads in the App; phone calls kept only as call-log rows are not counted. `judge_failures` counts the Fail verdicts of this workspace's own judges, and its volume is the number of verdicts in the window.

## Refusals

| Status | When |
| - | - |
| `400 validation_failed` | A field breaks a rule above, including a webhook target or tool that is not this workspace's own |
| `404 not_found` | No such rule in this workspace, a rule naming an agent the credential cannot read, or a body naming an agent it cannot reach |
| `409 conflict` | Evaluating a disabled rule |
| `409 conflict` | A conditional edit is stale: `reason: stale_alert` and the current `updated_at`; nothing is saved |
| `503 service_unavailable` | The deployment has no customer database to keep rules in. Only writes and `GET /v1/alerts/{id}` refuse this way: `GET /v1/alerts` answers an empty list and `GET /v1/alerts/metrics` still answers |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.