# Alerts and webhooks

> Hear when a goal or a funnel crosses a threshold through a signed webhook, and see what the traffic alert and report emails do today.

This page shows how to set up an alert that calls a webhook when a conversion goal or a funnel crosses a threshold, and how to check that a call really came from Clear Saga. It also covers the email reports and traffic alerts.

Everything here is under Settings, Reports and alerts of a site. An Owner or an Admin can change it.

## Email reports and traffic alerts

The section "Email notifications" has three switches. All start off.

> [!IMPORTANT]
> Email is not delivered yet. These messages are written and stored, and none reaches an inbox. See [what works today](/docs/status). The only alert that reaches you today is the webhook of a goal or funnel alert: traffic alerts have none, and nothing goes out by chat or text message.

- **Monthly report.** Written on the first of the month at 8 in the morning, in the time zone of the site, for the month before.
- **Weekly digest.** Written on Monday at the same hour, for the week before.
- **Traffic spike and drop alerts.** Once an hour, the page views of the last 24 hours are compared with the average of the 7 periods of the same length before them.

| Alert | Written when |
| --- | --- |
| Spike | The last period has at least 2.5 times the average, and at least 100 page views. |
| Drop | The last period has less than 50 percent of the average, and the average is at least 100 page views. |

A site is not judged until its first visit is 8 days old. Each kind of alert is written at most once per day. All three emails go to the address under "Send to", or to the Owner when it is empty.

## Alerts on a goal or a funnel

A rule in the section "Goal and funnel alerts" watches one [conversion goal](/docs/conversion-goals) or one [funnel](/docs/funnels) and calls an address of yours, a webhook, when its number crosses a line. Press "Add alert" and fill in:

| Field | What it means |
| --- | --- |
| Name | Sent with every call, so the receiver can tell rules apart. |
| Watch | The goal or the funnel. |
| Measure | A goal: its completions, or its conversion rate (out of all visitors). A funnel: the visitors who completed it, or its completion rate (out of those who entered). |
| Alert when it | Is below, or is above. |
| Threshold | A count, or a rate in percent. |
| Over | The last day or the last week (the keys 24h and 7d), ending at the start of the hour of the check. |
| Smallest sample | A rate on fewer visitors than this is not judged. It starts at 50. A count is always judged. |
| Remind every | While the rule stays breached, the alert is sent again after this many hours. It starts at 24. |
| Webhook address | Where the call goes. |

A site can hold 25 rules and a workspace 50. A Write token can manage rules through the [API](/docs/api/alerts) and the [command line](/docs/cli), but not through the [MCP server](/docs/mcp).

### When a rule is checked

Rules are checked every 60 minutes, and the calls a check queued are sent by a job every 5 minutes. So an alert can arrive more than an hour after the number moved, and later in a workspace with more than 25 rules or 10 waiting calls.

## The webhook

### The address

The address must start with `https://` and name a public host on the standard port. These are refused: a user name or password in the address, a bare IP address, a name without a dot, a name ending in .local, .internal, .localhost or .home.arpa, and any address of Clear Saga itself.

### The events

Each call is a `POST` with a JSON body. Its `type` is one of these:

- `alert.triggered`: A check found the rule breached: the measured value is on the wrong side of the threshold. Sent when that starts, and again after the cooldown of the rule while it lasts.
- `alert.recovered`: A check found a rule that was breached back on the right side of its threshold.
- `alert.test`: Someone asked for a test delivery of the rule. It carries the numbers of the last check and is never retried.

### The body

```json
{
  "type": "alert.triggered",
  "timestamp": "2026-03-02T09:00:00.000Z",
  "data": {
    "deliveryId": "ald_abc123def456",
    "ruleId": "alr_abc123def456",
    "ruleName": "Signups dried up",
    "site": { "id": "site_abc123def456", "domain": "example.com" },
    "subject": { "type": "goal", "id": "goal_abc123def456", "name": "Signup" },
    "metric": "goal_rate",
    "comparison": "below",
    "threshold": 0.02,
    "window": { "key": "24h", "from": 1772355600000, "to": 1772442000000 },
    "value": 0.011,
    "sample": 1840,
    "dashboardUrl": "https://clearsaga.com/app/site_abc123def456?range=24h&conversion=Signup",
    "mcpUrl": "https://clearsaga.com/mcp"
  }
}
```

The values are made up. A rate is a share from 0 to 1, in `threshold` and in `value`. `sample` is the number of visitors the rate is a share of. `from` and `to` are times in milliseconds. `metric` is goal_completions, goal_rate, funnel_completed or funnel_rate. `value` and `sample` can be `null`.

The body holds totals only, never a row about a visitor.

### Check the signature

Every call carries three headers: `webhook-id`, `webhook-timestamp` (seconds) and `webhook-signature`. The signature is `v1,` followed by the `base64` form of an `HMAC-SHA256` over the id, a dot, the timestamp, a dot and the body exactly as received. The key is the signing secret, decoded from `base64` after its `whsec_` prefix.

The signing secret is shown once, when the rule is created. To get a new one, edit the rule and tick "Issue a new signing secret". The old one stops working at once.

```js
import { createHmac, timingSafeEqual } from 'node:crypto'

// rawBody is the text of the request as it arrived, before any JSON parsing.
export function verify(secret, headers, rawBody) {
  const id = headers['webhook-id'], timestamp = headers['webhook-timestamp']
  // The allowed age of a call is your choice. This refuses one older than five minutes.
  if (!id || !timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false
  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64')
  const expected = createHmac('sha256', key).update(`${id}.${timestamp}.${rawBody}`).digest()
  return String(headers['webhook-signature']).split(' ').some((part) => {
    const [version, signature = ''] = part.split(',')
    const got = Buffer.from(signature, 'base64')
    return version === 'v1' && got.length === expected.length && timingSafeEqual(got, expected)
  })
}
```

The `webhook-id` stays the same when a call is sent again, so use it to ignore a repeat. A sending that was cut short is repeated after 15 minutes.

## Verify the endpoint first

A new rule is unverified until its address answers a call with a 2xx status. Until then, the first failed call switches the rule off at once, with no retry.

So send a test right after adding a rule: its menu has "Verify endpoint". On a verified rule the same action is named "Send test".

- A rule can have one test waiting, and 10 tests an hour.
- A rule that is off can be tested too. A 2xx answer switches it back on if it was off for being unverified, never if a person switched it off.
- A new address has to be verified again.

## Retries, and when a rule is switched off

Your endpoint has 10 seconds to answer. Anything but a 2xx status is a failure: another status, no answer in time, a network error or a redirect. The body of your answer is never read.

For a verified rule, a failed call is retried after 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours.

A rule is switched off when:

- 20 calls in a row have failed, each after its last retry.
- The endpoint answers `410 Gone`, which asks us to stop.
- Its address can no longer be used.

The list says why a rule is off. "Deliveries" in the menu of a rule shows its latest calls and how each one ended. The newest 200 are kept.

Deleting a goal or a funnel also deletes the rules that watch it, with their delivery logs and signing secrets.

## Locked workspaces

While the dashboards of a workspace are locked by its plan, its rules are not checked, so no alert is raised. A test can still be sent, without the measured numbers. [Plans, usage and limits](/docs/plans-and-usage) explains the lock.

**Next:** [Conversion goals](/docs/conversion-goals), [Funnels](/docs/funnels), [Connect an AI agent](/docs/agents)
