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

# Create and manage alert rules

> Watch yield, perps, watchlist, and protocol events; get notified when conditions fire.

You want a notification the moment an opportunity's APY drops below a threshold — or a curator, vault, or oracle event fires — without polling for it. Alert rules watch a data source server-side and dispatch to your channels (or just your in-app inbox) when every condition matches.

<Note>
  Alert rules live under `/v1`, not `/v2`. Responses are the resource itself (`AlertRule`, `AlertRuleList`, …), not wrapped in the `{data, meta}` [envelope](/develop/concepts/response-envelope) that `/v2` endpoints use.
</Note>

## What you build

A rule that fires on APY drop, plus the list/update/pause/delete/audit operations you need once it's live.

## 1. Create a rule

Creating and modifying rules requires the `write:alerts` scope (which also grants `read:alerts` — see [Scopes](/develop/get-started/scopes)).

<CodeGroup>
  ```bash curl theme={null}
  curl -H "Authorization: Bearer $ALTERSCOPE_API_KEY" \
       -H "Content-Type: application/json" \
       -X POST -d '{
         "name": "APY drop on USDC vault",
         "data_source": "yield_radar",
         "asset_symbol": "USDC",
         "conditions": [
           { "field": "apy", "operator": "lt", "value": 5.0 }
         ],
         "message_template": "{{.asset_symbol}} APY fell to {{.apy}}%",
         "cooldown_minutes": 60
       }' \
    "https://api.alterscope.org/v1/alerts/rules"
  ```

  ```python python theme={null}
  from alterscope import AlterscopeClient

  client = AlterscopeClient(api_key="sk_live_...")

  rule = client.post("/v1/alerts/rules", json={
      "name": "APY drop on USDC vault",
      "data_source": "yield_radar",
      "asset_symbol": "USDC",
      "conditions": [
          {"field": "apy", "operator": "lt", "value": 5.0},
      ],
      "message_template": "{{.asset_symbol}} APY fell to {{.apy}}%",
      "cooldown_minutes": 60,
  })
  print(rule["id"], rule["enabled"])  # a new rule is created enabled
  ```
</CodeGroup>

`data_source` is one of `yield_radar`, `hyperliquid`, `watchlist`, `vault_events`, `curator_events`, `oracle_events`, and it cannot be changed after creation. `conditions` is evaluated as AND — every condition must match for the rule to fire. `channel_ids` is optional and omitted here: an empty or absent list means in-app notification only. To fan out to email or webhook channels, create those channels first in the dashboard under **Settings → Notifications**, then pass their IDs — channel management itself is not part of this API.

A `404` on create means one or more `channel_ids` don't belong to you; a `409` means a rule with that `name` already exists; a `400` means the conditions or noise-control fields (`cooldown_minutes`, `require_persistence_minutes`, `dedupe_window_minutes`, `quiet_hours_*`) failed validation.

## 2. List and inspect rules

<CodeGroup>
  ```bash curl theme={null}
  curl -H "Authorization: Bearer $ALTERSCOPE_API_KEY" \
    "https://api.alterscope.org/v1/alerts/rules?enabled=true"
  ```

  ```python python theme={null}
  rules = client.get("/v1/alerts/rules", params={"enabled": True})["rules"]

  rule = client.get(f"/v1/alerts/rules/{rules[0]['id']}")
  ```
</CodeGroup>

`enabled` is an optional query filter on the list endpoint; omit it to get every rule regardless of state.

## 3. Update a rule — partial-merge PUT

`PUT /v1/alerts/rules/{id}` is a **partial merge**, not a full replace: only the fields present in the body change, and everything you omit keeps its current value. `data_source` is never accepted here.

The trap: "present" means present in the JSON, not "non-empty". If you send `"channel_ids": []`, that's an explicit instruction to clear the channels — different from leaving `channel_ids` out entirely, which leaves them untouched. The same applies to `conditions`: there's no per-condition merge, so sending `conditions` replaces the **whole array**. To add one condition to an existing rule, resend all of its existing conditions plus the new one — send just the new one and you silently drop the rest.

```bash curl theme={null}
# Only cooldown_minutes and message_template change; everything else
# on the rule — including its existing conditions — is untouched.
curl -H "Authorization: Bearer $ALTERSCOPE_API_KEY" \
     -H "Content-Type: application/json" \
     -X PUT -d '{
       "cooldown_minutes": 120,
       "message_template": "{{.asset_symbol}} APY dropped to {{.apy}}% (re-check before acting)"
     }' \
  "https://api.alterscope.org/v1/alerts/rules/$RULE_ID"
```

## 4. Pause or resume without touching the definition

Toggling doesn't count as an update to the rule's conditions, channels, or template — use it for a temporary pause instead of re-sending the full rule.

```bash curl theme={null}
curl -H "Authorization: Bearer $ALTERSCOPE_API_KEY" \
     -H "Content-Type: application/json" \
     -X POST -d '{"enabled": false}' \
  "https://api.alterscope.org/v1/alerts/rules/$RULE_ID/toggle"
```

```python python theme={null}
client.post(f"/v1/alerts/rules/{rule['id']}/toggle", json={"enabled": False})
```

## 5. Delete a rule

```bash curl theme={null}
curl -H "Authorization: Bearer $ALTERSCOPE_API_KEY" \
     -X DELETE \
  "https://api.alterscope.org/v1/alerts/rules/$RULE_ID"
```

A successful delete returns `204` with no body. Deleting is permanent — there's no undo endpoint; recreate the rule if you delete it by mistake.

## 6. Read the fire history

`GET /v1/alerts/events` returns your rule firings, most recent first — useful for confirming a rule actually fired, or for building your own audit log.

<CodeGroup>
  ```bash curl theme={null}
  curl -H "Authorization: Bearer $ALTERSCOPE_API_KEY" \
    "https://api.alterscope.org/v1/alerts/events?rule_id=$RULE_ID&limit=20"
  ```

  ```python python theme={null}
  events = client.get("/v1/alerts/events", params={
      "rule_id": rule["id"],
      "limit": 20,
  })["data"]

  for e in events:
      print(e["triggered_at"], e["status"], e["data_snapshot"])
  ```
</CodeGroup>

Each event carries `data_snapshot` (the observation that triggered the rule), `status` (`sent`, `suppressed`, or `failed`), and a `dispatches` array with one entry per channel — each with its own `status`, `sent_at`, and `error_message` if delivery failed. Filter by `status`, or by `start_date`/`end_date` (RFC3339) to bound a window; `limit` maxes out at 1000.

## 7. Troubleshooting

* **`403 insufficient_scope` on read-only calls**: list/get on rules and events only need `read:alerts`; a `write:alerts` key also has it, but a `read:alerts`-only key can't create, update, toggle, or delete.
* **A `PUT` didn't change what you expected**: re-read [step 3](#3-update-a-rule-partial-merge-put) — you likely sent a field that overwrote something you meant to leave alone (or omitted one you meant to change).
* **Rule doesn't fire**: check `enabled` (toggle may have flipped it), confirm `cooldown_minutes` or `quiet_hours_*` isn't currently suppressing it, and pull `/v1/alerts/events?rule_id=...&status=suppressed` to see suppressed-not-silent fires.
* **Rule fires but you get no notification**: check the event's `dispatches[].status` — a channel-side failure (bad webhook URL, bounced email) shows there with `error_message`, even though the rule itself fired.
