# Price alerts

Create, list, update, and cancel price alerts with the Python client.alerts service.

> **In development, coming soon**
>
> Price alerts are not yet available on Testnet: these calls fail there until the service is routed. This page documents the SDK surface so you can prepare for it.

`client.alerts` manages price alerts. An alert watches one market's last traded price or index price and fires once when the price crosses its level: `"above"` fires at or above the price, `"below"` at or below it. A fired alert is delivered in-app, to the account's linked Telegram account, or both.

Alerts belong to the main account. Use an API key of the main account; a key scoped to a sub-account raises `PolyesterAuthError` with `code` `SUBACCOUNT_KEY_REFUSED`.

## Methods

- **`create(...)`**: Create an alert on one market (`symbol` or `symbol_id`). Returns `PriceAlert`.
- **`list(...)`**: One page of your alerts, newest first. Returns `PriceAlertsPage`.
- **`iter_alerts(...)`**: Every alert matching the `list` filters, fetching pages as needed.
- **`update(...)`**: Change the direction, price or channels. Returns `PriceAlertUpdate`.
- **`cancel(alert_id=...)`**: Cancel an active alert. Returns `PriceAlert`.

### Create

```python
alert = await client.alerts.create(
    symbol="BTC-USDT",
    direction="above",
    price="70000",
    channels=["in_app", "telegram"],
    price_source="last",  # or "index"; leave out to use the index price where the market has one
    idempotency_key="btc-70k-alert",
)
print(alert.alert_id, alert.status, alert.armed, alert.reference_price)
```

`price` takes a decimal string, a `Decimal` or a `Price`. A `Price` bound to another market is refused before sending. `channels` names one or both of `"in_app"` and `"telegram"`. The current price is recorded as `reference_price`; an alert whose condition already holds starts with `armed` false and fires only after the price moves back and crosses again.

### List

```python
page = await client.alerts.list(statuses=["active"], symbol="BTC-USDT", limit=50)
for item in page.alerts:
    print(item.alert_id, item.direction, item.price, item.status)

async for item in client.alerts.iter_alerts(statuses=["triggered"]):
    print(item.alert_id, item.triggered_at, item.triggered_price)
```

`statuses` takes `"active"`, `"triggered"` and `"cancelled"`. Note the spelling: alerts use `"cancelled"`, unlike a trigger's `"canceled"`. `limit` is at most 200. Pass `next_page_token` back as `page_token` with the same filters.

### Update and cancel

```python
result = await client.alerts.update(
    alert_id=alert.alert_id,
    expected_revision=alert.revision,
    price="72000",
    idempotency_key="btc-72k-alert",
)
print(result.outcome, result.alert.revision)

cancelled = await client.alerts.cancel(alert_id=alert.alert_id)
```

`update` changes only the fields you pass. With none, it sets a triggered alert again as it is. `expected_revision` is the `revision` from your last read; a stale one is refused with `REVISION_CONFLICT`. `outcome` is `"applied"`, `"unchanged"` or `"replayed"` (the idempotency key was already used for this request). Cancelling a cancelled alert returns it unchanged; a triggered alert is refused with `ALERT_TRIGGERED`, so update it to set it again.

## Errors and retries

- **`PRICE_UNAVAILABLE`**: a retryable `PolyesterServerError`; nothing changed.
- **`SERVICE_UNAVAILABLE`** on a create, update or cancel: the outcome is unknown. Send a create or update again with the same `idempotency_key`, cancel again, or read the alerts back.
- **`IDEMPOTENCY_CONFLICT`**: the key was used for another request; the original stands.
- Other refusals raise `PolyesterApiError` with the contract code as `code`.

## Related

- [Errors](https://testnet.polyester.com/docs/sdk/python/reference/errors)
- [Social verification](https://testnet.polyester.com/docs/sdk/python/reference/social-verification) for the Telegram link
