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

# Configure alerts

> Create rank-change rules with in-app, email, Slack, and signed webhook delivery.

Alerts are evaluated after a completed rank check. Creating a rule does not
immediately query a SERP provider or evaluate historical checks.

## Create and verify a webhook endpoint

1. Open **Alerts**, select **New rule**, and choose **Webhook** under Delivery.
2. Enter an HTTP or HTTPS endpoint URL.
3. Select **Generate** to create a strong HMAC secret, then copy and store it.
   The encrypted secret cannot be read back after the endpoint is saved.
4. Save the enabled endpoint.
5. Select **Send test event** in its endpoint row.
6. Confirm that the inline result shows the receiver's HTTP status and latency.
7. Verify `X-Bisibility-Signature` against the raw request body with the
   [copy-pasteable verifier](/docs/docs/api/webhooks#verify-signatures).

The test is a real signed `alert.fired` envelope with `data.test: true`.
After it succeeds, create a webhook-channel rule. Every enabled project endpoint
receives every webhook event; endpoints are not filtered by rule or event name.

## Create a rule

1. Open **Alerts** and select **New rule**.
2. Choose all keywords, one keyword, or one tag as the scope.
3. Select a condition and enter its threshold when required.
4. Select one or more delivery channels.
5. Save the rule and leave it enabled.

| Condition             | Fires when                                                                 |
| --------------------- | -------------------------------------------------------------------------- |
| Threshold             | Position crosses the configured rank.                                      |
| Change percent        | The relative position change reaches the configured percentage.            |
| Position drop         | A keyword loses at least the configured number of positions.               |
| Downtrend             | Five complete checks with known positions form a sustained downward trend. |
| Enters or exits top N | Position crosses a configured top-N boundary.                              |
| Competitor overtake   | The configured competitor moves ahead.                                     |
| SERP feature          | The requested feature appears in the provider payload.                     |
| URL mismatch          | The ranking URL differs from the keyword target URL.                       |

An incomplete downtrend window or an unranked check with no known position is
treated as unknown, not as recovery. An existing downtrend alert remains open
until five complete checks show that the condition cleared. It can be manually
muted while the state is unknown.

## Delivery channels

* **In-app:** successful external delivery creates an in-app notification and
  the triggered alert remains visible in the alert feed.
* **Email:** requires a configured [email provider](/docs/docs/guides/email) with a
  verified sender (`EMAIL_FROM`). A user's alert-email preference can disable
  delivery.
* **Slack:** requires a connected and enabled Slack channel.
* **Webhook:** sends signed JSON to every enabled project endpoint when the rule
  includes the webhook channel.

Webhook targets resolving to loopback, link-local, or private networks are
blocked by default. `WEBHOOK_ALLOW_PRIVATE_NETWORK=1` is limited to trusted
self-hosted networks.

Delivery attempts are recorded for every channel. A failed channel does not
prevent the remaining selected channels from being attempted.

## Webhook events

Every outbound webhook is an HTTP `POST` with `Content-Type: application/json`,
`X-Bisibility-Timestamp` (Unix seconds), and
`X-Bisibility-Signature: sha256=<hex digest>`. The signature is an HMAC-SHA256
of `<timestamp>.<raw request body>` using the endpoint secret. Verify the
signature against the raw body before parsing JSON.

An immediate alert uses `alert.fired`. Manual checks and other non-scheduled
checks stay immediate:

```json theme={null}
{
  "event": "alert.fired",
  "createdAt": "2026-07-21T09:00:03.000Z",
  "data": {
    "alertId": "cmc9s71qk0001qzld9b0v5x2a",
    "keyword": "rank tracker",
    "keywordId": "kw_01J2QK8Z9XR6E7S4G5H6J7K8L9",
    "beforePosition": 4,
    "afterPosition": 11,
    "conditionType": "position_drop",
    "firedAt": "2026-07-21T09:00:00.000Z",
    "headline": "Rank tracker dropped from #4 to #11",
    "action": "Review the ranking page and recent SERP changes.",
    "projectId": "cmc9s6k3b0000qzldf7a2n8w4",
    "projectDomain": "example.com",
    "ruleId": "cmc9s7m5t0002qzld1h6r3y8p",
    "ruleName": "Important position drops"
  }
}
```

### Digest event

Scheduled checks queue matching alerts until the digest flush runs. The default
flush interval is five minutes and can be changed with
`ALERT_DIGEST_FLUSH_INTERVAL`. Alerts are grouped by rule, then one digest is
delivered to each configured channel and enabled webhook endpoint. This means a
consumer can receive one `alert.digest` containing multiple alerts for a
rule-and-endpoint pair during a flush.

`alert.digest` uses this JSON schema. `data.alerts` contains the complete batch
in the delivery, and every item has the same shape as `alert.fired.data`.

```json theme={null}
{
  "event": "alert.digest",
  "createdAt": "2026-07-21T09:05:00.000Z",
  "data": {
    "alertCount": 2,
    "alerts": [
      {
        "alertId": "cmc9s71qk0001qzld9b0v5x2a",
        "keyword": "rank tracker",
        "keywordId": "kw_01J2QK8Z9XR6E7S4G5H6J7K8L9",
        "beforePosition": 4,
        "afterPosition": 11,
        "conditionType": "position_drop",
        "firedAt": "2026-07-21T09:00:00.000Z",
        "headline": "Rank tracker dropped from #4 to #11",
        "action": "Review the ranking page and recent SERP changes.",
        "projectId": "cmc9s6k3b0000qzldf7a2n8w4",
        "projectDomain": "example.com",
        "ruleId": "cmc9s7m5t0002qzld1h6r3y8p",
        "ruleName": "Important position drops"
      },
      {
        "alertId": "cmc9s7w9h0003qzld4k8p6t1v",
        "keyword": "keyword rank checker",
        "keywordId": "kw_01J2QMKC0VQ1W2X3Y4Z5A6B7C8",
        "beforePosition": 8,
        "afterPosition": 16,
        "conditionType": "position_drop",
        "firedAt": "2026-07-21T09:02:00.000Z",
        "headline": "Keyword rank checker dropped from #8 to #16",
        "action": "Review the ranking page and recent SERP changes.",
        "projectId": "cmc9s6k3b0000qzldf7a2n8w4",
        "projectDomain": "example.com",
        "ruleId": "cmc9s7m5t0002qzld1h6r3y8p",
        "ruleName": "Important position drops"
      }
    ],
    "conditionType": "position_drop",
    "projectId": "cmc9s6k3b0000qzldf7a2n8w4",
    "projectDomain": "example.com",
    "ruleId": "cmc9s7m5t0002qzld1h6r3y8p",
    "ruleName": "Important position drops",
    "suppressedTodayCount": 0
  }
}
```

Digest jobs contain at most 200 alerts. Any remaining pending alerts wait for a
later flush. Email and Slack summaries display the first 20 entries and a
`+N more` line, but webhook `data.alerts` is not shortened. `alertCount` always
matches the length of `data.alerts`.

Each rule has a daily budget of 20 delivery batches. An immediate alert or one
digest consumes one batch regardless of the number of recipients and channels.
One batch can fan out to every email recipient, enabled webhook endpoint, and
the connected Slack channel. When the budget is exhausted, pending alerts are
suppressed until 00:00 UTC and remain visible in the app. A webhook endpoint
receives one `alert.daily_cap_reached` notice for that rule and day; its data
contains `projectId`, `ruleId`, `ruleName`, and `suppressedCount`.
`suppressedTodayCount` on a later digest reports alerts already suppressed for
that rule on the current UTC day.

### Migration for webhook consumers

Keep handling `alert.fired` as one alert per request. Add an `alert.digest`
branch that verifies the same signature and iterates `data.alerts`; do not treat
the digest object itself as a single alert. Consumers that only support
`alert.fired` should ignore unknown event types until they are updated. Handle
`alert.daily_cap_reached` separately if the integration needs to surface
delivery suppression.

## Operate the feed

Use the feed filters for unread and urgent alerts. **Mark all read** acknowledges
the current project feed. Snoozing hides a triggered alert until its snooze
expires; it does not disable the underlying rule. Pause or delete the rule to
stop future evaluation.

API automation is documented under
[Alert rules](/docs/docs/api/alert-rules). The complete endpoint, event, signing,
timeout, and retry contract is under [Webhook endpoints](/docs/docs/api/webhooks).
