# Alert rules

> The API operations about alert rules, each with its input, its output and an example.

The operations about alert rules. Each one is a `POST` with a JSON object as its body: [Making a request](/docs/api#start) has the rules they share.

## Create alert rule

`POST /api/v1/alerts.create` · Write

Adds an alert rule to one site: a goal or a funnel, a metric, a comparison with a threshold, a trailing window and a webhook address. Rules are checked once an hour. When the value crosses the threshold a delivery with type alert.triggered is queued, again after cooldownHours while it stays breached, and alert.recovered when it is fine again. Deliveries are sent every five minutes, signed as Standard Webhooks describes. The answer carries webhookSecret, the signing secret, this once. The webhook address must be https on the default port with a public host name: no bare IP address, no user name or password, no localhost or internal name, and not an address of this service (400 invalid webhookUrl). Redirects are not followed. The endpoint starts unverified: call alerts.test to send a test, and the first 2xx answer verifies it. An unverified rule is switched off after its first failed delivery and is not retried. The metric must fit the subject (400 invalid metric), a rate threshold is at most 1 (400 invalid threshold), the subject must be a goal or funnel of this site (400 invalid subjectId). A site holds at most 25 rules and a workspace 50 (409 limit_reached). Needs a token with the Write permission.

### Input

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `siteId` | string | required | The site id, from sites.list. |
| `name` | string, 1 to 80 characters | required | A name for the rule, 1 to 80 characters. It is sent in every delivery as ruleName. |
| `subjectType` | "goal" \| "funnel" | required | What the rule watches: goal (a conversion goal) or funnel (a saved funnel). |
| `subjectId` | string, 1 to 64 characters | required | The id of the goal (from goals.list) or of the funnel (from funnels.list). It must belong to this site. |
| `metric` | "goal_completions" \| "goal_rate" \| "funnel_completed" \| "funnel_rate" | required | What is measured. For a goal: goal_completions (how often it was completed) or goal_rate (visitors who completed it divided by all visitors). For a funnel: funnel_completed (visitors who did every step) or funnel_rate (completed divided by entered). The metric must fit subjectType. |
| `comparison` | "below" \| "above" | required | below: the rule is breached while the value is below the threshold. above: while it is above. |
| `threshold` | number, 0 to 1000000000 | required | The number to compare with. A count for goal_completions and funnel_completed; a fraction from 0 to 1 for the two rates, for example 0.02 for 2 percent. |
| `window` | "24h" \| "7d" | required | The trailing period the value is measured over: 24h or 7d, ending at the start of the hour of each check. |
| `minSample` | integer, 1 to 1000000 | optional | For the two rates: the fewest visitors (goal_rate) or entered visitors (funnel_rate) the rate is judged on. With fewer the rule has no data and does not fire. 1 to 1,000,000; 50 when left out on create. Counts ignore it. Default: `50`. |
| `cooldownHours` | integer, 1 to 720 | optional | While the rule stays breached, alert.triggered is sent again after this many hours. 1 to 720; 24 when left out on create. Default: `24`. |
| `webhookUrl` | string, at most 2048 characters | required | Where deliveries are sent, as an HTTP POST with a JSON body. The webhook address must be https on the default port with a public host name: no bare IP address, no user name or password, no localhost or internal name, and not an address of this service (400 invalid webhookUrl). Redirects are not followed. Treat the address as a secret. |
| `enabled` | boolean | optional | Whether the rule is checked. True when left out on create. Sending true for a rule the system switched off switches it on again. Default: `true`. |

### Output

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `id` | string | always | The rule id. Pass it to alerts.update, alerts.test and alerts.deliveries as id. |
| `name` | string | always | The name the rule was saved under. |
| `subject` | object | always | What the rule watches: type (goal or funnel), its id, and its name. The name is empty when the goal or funnel no longer exists. |
| `metric` | "goal_completions" \| "goal_rate" \| "funnel_completed" \| "funnel_rate" | always | What is measured: goal_completions (a count), goal_rate (converters divided by visitors), funnel_completed (visitors who did every step) or funnel_rate (completed divided by entered). |
| `comparison` | "below" \| "above" | always | Whether the rule is breached while the value is below the threshold or above it. |
| `threshold` | number | always | The number the value is compared with: a count, or for a rate a fraction from 0 to 1. |
| `window` | "24h" \| "7d" | always | The trailing period the value is measured over, ending at the start of the hour of each check. |
| `minSample` | number | always | The fewest visitors (for a goal rate) or entered visitors (for a funnel rate) a rate is judged on. With fewer the rule has no data and is never breached. Counts ignore it. |
| `cooldownHours` | number | always | While the rule stays breached, alert.triggered is sent again after this many hours. |
| `webhookUrl` | string \| null | always | The address deliveries are sent to. Null for a token without the Write permission: the address is a secret, and such a caller gets webhookHost only. |
| `webhookHost` | string | always | The host name of the webhook address. |
| `enabled` | boolean | always | Whether the rule is checked. A rule that was switched off by the system says why in disabledReason. |
| `state` | "fine" \| "breached" \| "no_data" | always | What the last check found: fine, breached, or no_data (too small a sample for a rate, or a goal that is switched off). |
| `lastValue` | number \| null | always | The value at the last check, or null before the first check and when there was nothing to divide by. |
| `lastSample` | number \| null | always | The sample at the last check: visitors for a goal, entered visitors for a funnel. Null before the first check. |
| `lastEvaluatedAt` | number \| null | always | When the rule was last checked, milliseconds since 1970 UTC. Null before the first check. Rules are checked once an hour. |
| `lastFiredAt` | number \| null | always | When alert.triggered was last queued, milliseconds since 1970 UTC. Null when it never was. |
| `verified` | boolean | always | Whether the endpoint has ever answered a delivery with a 2xx status. An unverified rule is switched off after its first failed delivery. |
| `disabledReason` | "subject_deleted" \| "endpoint_failing" \| "endpoint_gone" \| "endpoint_unverified" \| null | always | Why the system switched the rule off, else null: subject_deleted (the goal or funnel is gone), endpoint_failing (20 failed deliveries in a row), endpoint_gone (the endpoint answered 410), endpoint_unverified (the endpoint failed before it ever answered). |
| `createdAt` | number | always | When the rule was created, milliseconds since 1970 UTC. |
| `updatedAt` | number | always | When the rule was last changed, milliseconds since 1970 UTC. |
| `webhookSecret` | string | always | The signing secret of the rule: whsec\_ followed by base64. Deliveries are signed with it as Standard Webhooks describes (HMAC-SHA256 over id.timestamp.body, keyed with the bytes after whsec\_). It is shown this once and cannot be read again; store it now. |

### Example

**curl**

```bash
curl -s -X POST https://clearsaga.com/api/v1/alerts.create \
  -H "Authorization: Bearer $CLEARSAGA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"siteId":"site_abc123def456","name":"Signup rate dropped","subjectType":"goal","subjectId":"GOAL_ID","metric":"goal_rate","comparison":"below","threshold":0.02,"window":"7d","webhookUrl":"https://example.com/hooks/alerts"}'
```

**CLI**

```bash
clearsaga alerts create --input '{"siteId":"site_abc123def456","name":"Signup rate dropped","subjectType":"goal","subjectId":"GOAL_ID","metric":"goal_rate","comparison":"below","threshold":0.02,"window":"7d","webhookUrl":"https://example.com/hooks/alerts"}'
```

**MCP**

```text
Not offered as an MCP tool: Makes our server call an address the caller chooses. Left to people and their own programs: use the API, the CLI or the dashboard.
```

Run `clearsaga help alerts create` for one flag per field.

## Delete alert rule

`POST /api/v1/alerts.delete` · Write

Removes an alert rule from one site for good, together with its delivery log (what alerts.deliveries lists) and its signing secret. This cannot be undone. To pause a rule and keep its log, call alerts.update with enabled false instead. The id comes from alerts.list. The rule is checked no more. Deliveries that were still waiting are removed with it and none is queued after that; only one that is being sent at that very moment can still arrive. The goal or funnel the rule watched is not touched, and neither is any report. An id the site does not have is not an error: the answer says deleted false and nothing was changed, so sending the same call twice is safe. Needs a token with the Write permission.

### Input

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `siteId` | string | required | The site id, from sites.list. |
| `id` | string, at least 1 characters | required | The id of the rule to remove, from alerts.list. |

### Output

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `id` | string | always | The id that was sent. |
| `deleted` | boolean | always | true when the alert rule was there and this call removed it. false when the site has no alert rule with that id, because it was removed before or the id is wrong: nothing was changed, and that is not an error. |
| `name` | string \| null | always | The name the rule had, so it is clear what was removed. Null when deleted is false. |

### Example

**curl**

```bash
curl -s -X POST https://clearsaga.com/api/v1/alerts.delete \
  -H "Authorization: Bearer $CLEARSAGA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"siteId":"site_abc123def456","id":"ALERT_ID"}'
```

**CLI**

```bash
clearsaga alerts delete --input '{"siteId":"site_abc123def456","id":"ALERT_ID"}'
```

**MCP**

```text
Not offered as an MCP tool: Alert rules are created, changed and removed outside MCP, by people and their own programs: use the API, the CLI or the dashboard.
```

Run `clearsaga help alerts delete` for one flag per field.

## List alert deliveries

`POST /api/v1/alerts.deliveries` · Read

The webhook deliveries of one alert rule, newest first: what was sent (triggered, recovered or a test), the value at that moment, and how the sending went (status, attempts, the last HTTP status, when it is tried next). A failed delivery to a verified endpoint is retried with growing gaps for about a day and a half; an endpoint that never answered is not retried. The newest 200 deliveries of a rule are kept. The rows never carry the webhook address or the body that was sent. An unknown id answers 404. Keeps working while the workspace is locked.

Needs a token with the Read permission. Keeps answering while the dashboards of the workspace are locked.

### Input

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `siteId` | string | required | The site id, from sites.list. |
| `id` | string, at least 1 characters | required | The id of the rule, from alerts.list. |
| `limit` | integer, 1 to 100 | optional | How many deliveries to answer, 1 to 100. 100 when left out. |

### Output

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `rows` | array of object | always | The deliveries of the rule, newest first. Each row is a delivery as alerts.test answers it. |

### Example

**curl**

```bash
curl -s -X POST https://clearsaga.com/api/v1/alerts.deliveries \
  -H "Authorization: Bearer $CLEARSAGA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"siteId":"site_abc123def456","id":"ALERT_ID"}'
```

**CLI**

```bash
clearsaga alerts deliveries --input '{"siteId":"site_abc123def456","id":"ALERT_ID"}'
```

**MCP**

```text
Tool: alerts_deliveries
Arguments: {"siteId":"site_abc123def456","id":"ALERT_ID"}
```

Run `clearsaga help alerts deliveries` for one flag per field.

## List alert rules

`POST /api/v1/alerts.list` · Read

Lists the alert rules of one site, oldest first. A rule watches one conversion goal or one funnel over the last 24 hours or 7 days and calls a webhook when its number crosses a threshold. Each row says what the last hourly check found (state, lastValue, lastSample), when the rule last fired, whether its endpoint is verified and, when the system switched it off, why. The full webhook address is only in the answer for a token with the Write permission; a read token gets webhookUrl null and the host name in webhookHost. The signing secret is never listed. Keeps working while the workspace is locked.

Needs a token with the Read permission. Keeps answering while the dashboards of the workspace are locked.

### Input

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `siteId` | string | required | The site id, from sites.list. |

### Output

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `rows` | array of object | always | One row per alert rule of the site, oldest first. Each row is a rule as alerts.create answers it, without the signing secret. |

### Example

**curl**

```bash
curl -s -X POST https://clearsaga.com/api/v1/alerts.list \
  -H "Authorization: Bearer $CLEARSAGA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"siteId":"site_abc123def456"}'
```

**CLI**

```bash
clearsaga alerts list --input '{"siteId":"site_abc123def456"}'
```

**MCP**

```text
Tool: alerts_list
Arguments: {"siteId":"site_abc123def456"}
```

Run `clearsaga help alerts list` for one flag per field.

## Send test delivery

`POST /api/v1/alerts.test` · Write

Queues one test delivery (type alert.test) for an alert rule, with the numbers of its last check, and answers the delivery row with status pending. Nothing is sent during this call: deliveries are sent every five minutes, so read alerts.deliveries afterwards to see whether it arrived. A 2xx answer to a test verifies the endpoint, and switches a rule back on that was off only because its endpoint had never answered. A failed test to an unverified endpoint switches the rule off. A test is never retried. One test per rule can wait at a time (409 limit_reached test), and a rule takes at most 10 tests an hour (429 rate_limited). An unknown id answers 404. Needs a token with the Write permission.

### Input

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `siteId` | string | required | The site id, from sites.list. |
| `id` | string, at least 1 characters | required | The id of the rule, from alerts.list. |

### Output

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `id` | string | always | The delivery id. It is sent as the webhook-id header and stays the same across retries. |
| `event` | "alert.triggered" \| "alert.recovered" \| "alert.test" | always | What the delivery says: the rule became breached (or is still breached after the cooldown), it went back to fine, or it is a test. |
| `firedAt` | number | always | When the event happened, milliseconds since 1970 UTC. |
| `value` | number \| null | always | The value of the rule at that moment, or null when it had none. |
| `sample` | number \| null | always | The sample at that moment, or null when it had none. |
| `threshold` | number | always | The threshold of the rule at that moment. |
| `status` | "pending" \| "sending" \| "delivered" \| "failed" | always | pending: waiting to be sent or retried. sending: being sent right now. delivered: the endpoint answered 2xx. failed: given up. |
| `attempts` | number | always | How many times the endpoint was called for this delivery. |
| `lastStatusCode` | number \| null | always | The HTTP status of the last call, or null when no answer arrived (a timeout, a network error) or nothing was sent yet. |
| `nextAttemptAt` | number \| null | always | When a pending delivery is sent next, milliseconds since 1970 UTC. Deliveries are sent every five minutes. Null otherwise. |
| `deliveredAt` | number \| null | always | When the endpoint answered 2xx, milliseconds since 1970 UTC, else null. |

### Example

**curl**

```bash
curl -s -X POST https://clearsaga.com/api/v1/alerts.test \
  -H "Authorization: Bearer $CLEARSAGA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"siteId":"site_abc123def456","id":"ALERT_ID"}'
```

**CLI**

```bash
clearsaga alerts test --input '{"siteId":"site_abc123def456","id":"ALERT_ID"}'
```

**MCP**

```text
Not offered as an MCP tool: Makes our server call an address the caller chooses. Left to people and their own programs: use the API, the CLI or the dashboard.
```

Run `clearsaga help alerts test` for one flag per field.

## Update alert rule

`POST /api/v1/alerts.update` · Write

Changes one alert rule. Send id and only the fields to change; a field left out keeps its value. Changing what is measured (subjectType, subjectId, metric, comparison, threshold, window, minSample) starts the rule over: its state goes back to fine and the next hourly check judges the new condition. A new webhookUrl makes the endpoint unverified again until a delivery to it gets a 2xx answer. regenerateSecret true issues a new signing secret, which is in the answer as webhookSecret this once; the old one stops working at once. The webhook address must be https on the default port with a public host name: no bare IP address, no user name or password, no localhost or internal name, and not an address of this service (400 invalid webhookUrl). Redirects are not followed. The same value rules as alerts.create apply. An unknown id answers 404. Needs a token with the Write permission.

### Input

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `siteId` | string | required | The site id, from sites.list. |
| `id` | string, at least 1 characters | required | The id of the rule, from alerts.list. |
| `name` | string, 1 to 80 characters | optional | A name for the rule, 1 to 80 characters. It is sent in every delivery as ruleName. |
| `subjectType` | "goal" \| "funnel" | optional | What the rule watches: goal (a conversion goal) or funnel (a saved funnel). |
| `subjectId` | string, 1 to 64 characters | optional | The id of the goal (from goals.list) or of the funnel (from funnels.list). It must belong to this site. |
| `metric` | "goal_completions" \| "goal_rate" \| "funnel_completed" \| "funnel_rate" | optional | What is measured. For a goal: goal_completions (how often it was completed) or goal_rate (visitors who completed it divided by all visitors). For a funnel: funnel_completed (visitors who did every step) or funnel_rate (completed divided by entered). The metric must fit subjectType. |
| `comparison` | "below" \| "above" | optional | below: the rule is breached while the value is below the threshold. above: while it is above. |
| `threshold` | number, 0 to 1000000000 | optional | The number to compare with. A count for goal_completions and funnel_completed; a fraction from 0 to 1 for the two rates, for example 0.02 for 2 percent. |
| `window` | "24h" \| "7d" | optional | The trailing period the value is measured over: 24h or 7d, ending at the start of the hour of each check. |
| `minSample` | integer, 1 to 1000000 | optional | For the two rates: the fewest visitors (goal_rate) or entered visitors (funnel_rate) the rate is judged on. With fewer the rule has no data and does not fire. 1 to 1,000,000; 50 when left out on create. Counts ignore it. |
| `cooldownHours` | integer, 1 to 720 | optional | While the rule stays breached, alert.triggered is sent again after this many hours. 1 to 720; 24 when left out on create. |
| `webhookUrl` | string, at most 2048 characters | optional | Where deliveries are sent, as an HTTP POST with a JSON body. The webhook address must be https on the default port with a public host name: no bare IP address, no user name or password, no localhost or internal name, and not an address of this service (400 invalid webhookUrl). Redirects are not followed. Treat the address as a secret. |
| `enabled` | boolean | optional | Whether the rule is checked. True when left out on create. Sending true for a rule the system switched off switches it on again. |
| `regenerateSecret` | boolean | optional | True to issue a new signing secret. The answer carries it once and the old one stops working. |

### Output

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `id` | string | always | The rule id. Pass it to alerts.update, alerts.test and alerts.deliveries as id. |
| `name` | string | always | The name the rule was saved under. |
| `subject` | object | always | What the rule watches: type (goal or funnel), its id, and its name. The name is empty when the goal or funnel no longer exists. |
| `metric` | "goal_completions" \| "goal_rate" \| "funnel_completed" \| "funnel_rate" | always | What is measured: goal_completions (a count), goal_rate (converters divided by visitors), funnel_completed (visitors who did every step) or funnel_rate (completed divided by entered). |
| `comparison` | "below" \| "above" | always | Whether the rule is breached while the value is below the threshold or above it. |
| `threshold` | number | always | The number the value is compared with: a count, or for a rate a fraction from 0 to 1. |
| `window` | "24h" \| "7d" | always | The trailing period the value is measured over, ending at the start of the hour of each check. |
| `minSample` | number | always | The fewest visitors (for a goal rate) or entered visitors (for a funnel rate) a rate is judged on. With fewer the rule has no data and is never breached. Counts ignore it. |
| `cooldownHours` | number | always | While the rule stays breached, alert.triggered is sent again after this many hours. |
| `webhookUrl` | string \| null | always | The address deliveries are sent to. Null for a token without the Write permission: the address is a secret, and such a caller gets webhookHost only. |
| `webhookHost` | string | always | The host name of the webhook address. |
| `enabled` | boolean | always | Whether the rule is checked. A rule that was switched off by the system says why in disabledReason. |
| `state` | "fine" \| "breached" \| "no_data" | always | What the last check found: fine, breached, or no_data (too small a sample for a rate, or a goal that is switched off). |
| `lastValue` | number \| null | always | The value at the last check, or null before the first check and when there was nothing to divide by. |
| `lastSample` | number \| null | always | The sample at the last check: visitors for a goal, entered visitors for a funnel. Null before the first check. |
| `lastEvaluatedAt` | number \| null | always | When the rule was last checked, milliseconds since 1970 UTC. Null before the first check. Rules are checked once an hour. |
| `lastFiredAt` | number \| null | always | When alert.triggered was last queued, milliseconds since 1970 UTC. Null when it never was. |
| `verified` | boolean | always | Whether the endpoint has ever answered a delivery with a 2xx status. An unverified rule is switched off after its first failed delivery. |
| `disabledReason` | "subject_deleted" \| "endpoint_failing" \| "endpoint_gone" \| "endpoint_unverified" \| null | always | Why the system switched the rule off, else null: subject_deleted (the goal or funnel is gone), endpoint_failing (20 failed deliveries in a row), endpoint_gone (the endpoint answered 410), endpoint_unverified (the endpoint failed before it ever answered). |
| `createdAt` | number | always | When the rule was created, milliseconds since 1970 UTC. |
| `updatedAt` | number | always | When the rule was last changed, milliseconds since 1970 UTC. |
| `webhookSecret` | string | may be absent | Only present when regenerateSecret was true. The signing secret of the rule: whsec\_ followed by base64. Deliveries are signed with it as Standard Webhooks describes (HMAC-SHA256 over id.timestamp.body, keyed with the bytes after whsec\_). It is shown this once and cannot be read again; store it now. The old secret stops working at once. |

### Example

**curl**

```bash
curl -s -X POST https://clearsaga.com/api/v1/alerts.update \
  -H "Authorization: Bearer $CLEARSAGA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"siteId":"site_abc123def456","id":"ALERT_ID"}'
```

**CLI**

```bash
clearsaga alerts update --input '{"siteId":"site_abc123def456","id":"ALERT_ID"}'
```

**MCP**

```text
Not offered as an MCP tool: Makes our server call an address the caller chooses. Left to people and their own programs: use the API, the CLI or the dashboard.
```

Run `clearsaga help alerts update` for one flag per field.
