# Watchlist

URL: https://igregulator.io/docs/watchlist/
Markdown: https://igregulator.io/docs/watchlist.md

> Track specific operators for automated alerts on licence changes, expiries, and regulatory actions. Webhook push or polling fallback.

A watchlist is your list of operators you care about, plus the
automation layer that fires events when any of them changes. Add
an operator once, get alerted whenever its licence status flips,
a regulatory action lands, or an expiry date approaches — without
writing polling loops against every endpoint we offer.

## 1. Overview

Plan limits (also in [pricing](https://igregulator.io/#pricing)):

| Tier | Watchlist cap | Webhooks | Polling |
| --- | --- | --- | --- |
| Starter | 25 operators | 1 endpoint | 10 / hour |
| Pro | 250 operators | 5 endpoints | 60 / hour |
| Business | unlimited | 20 endpoints | 600 / hour |
| Enterprise | unlimited | unlimited | unlimited |

Webhooks are the primary alert channel. Polling exists for agents
that can't accept inbound HTTP (corporate networks, air-gapped
analytics, local dev) and as a backfill mechanism during webhook
outages.

## 2. Managing the watchlist

### Dashboard

Sign in and open
[app.igregulator.io/watchlist](https://app.igregulator.io/watchlist).
Type an operator name — we typeahead against every operator
slug we know about. Click to add. Click remove to drop.

### API

Same surface via bearer token:

```bash
# Current watchlist + count + plan cap (the 50 most recently added)
curl -H "Authorization: Bearer igk_..." \
  https://api.igregulator.io/v1/watchlist

# Add an operator by slug (lowercase, hyphens) — 201, or 409 if already watched
curl -X POST -H "Authorization: Bearer igk_..." \
  -H "Content-Type: application/json" \
  -d '{"operator_slug":"888-uk-limited"}' \
  https://api.igregulator.io/v1/watchlist/operators

# Remove (idempotent — 204 either way)
curl -X DELETE -H "Authorization: Bearer igk_..." \
  https://api.igregulator.io/v1/watchlist/operators/888-uk-limited

# Paginated listing with current licence status per operator
curl -H "Authorization: Bearer igk_..." \
  "https://api.igregulator.io/v1/watchlist/operators?limit=50&offset=0"
```

Discover slugs with `GET /v1/operators/search?q=<name>`.

## 3. Receiving events

### Webhook push (primary)

Create an endpoint with the `watchlist_only: true` flag (default).
Deliveries fire for operators in your watchlist only — no
firehose, no noise. Events covered:

- `license.status_changed`
- `license.expiring_30d` / `_60d` / `_90d`
- `license.expired`
- `license.issued` (only if you were watching the operator when
  the new licence was detected)
- `regulatory_action.added`

See [/docs/webhooks](https://igregulator.io/docs/webhooks/) for the signing + retry
protocol. Quickstart in 2 minutes:
[/docs/webhooks/quickstart](https://igregulator.io/docs/webhooks/quickstart/).

### Polling (fallback)

`GET /v1/watchlist/events` returns the same envelope events,
pulled. Cursor-paginated so you don't re-process events between
runs. Rate-limited per plan (see §1). Every response carries
`X-Poll-RateLimit-Limit`, `-Remaining`, `-Reset`, `-Window`, and a
`X-Poll-Recommended-Interval` hint in seconds (on a plan with no poll
ceiling: `X-Poll-RateLimit-Limit: unlimited`, `-Reset` and the interval
only). `limit` is 1–500 (default 100); with neither `since` nor
`cursor`, you get the last hour.

You don't need a webhook endpoint to poll: you get every event above for
the operators on your watchlist, plus anything that was queued for your
webhook endpoints (such as `coverage.*`, if an endpoint subscribes to it).

```bash
# Bootstrap: up to 30 days back (matches event retention; GNU date)
curl -H "Authorization: Bearer igk_..." \
  "https://api.igregulator.io/v1/watchlist/events?since=$(date -u -d '29 days ago' +%FT%TZ)&limit=100"

# Steady state: use the next_cursor from the previous response
curl -H "Authorization: Bearer igk_..." \
  "https://api.igregulator.io/v1/watchlist/events?cursor=eyJ0cy...&limit=100"
```

Response:

```json
{
  "events": [
    {
      "event": "license.status_changed",
      "event_id": "evt_...",
      "api_version": "2026-04-20",
      "timestamp": "...",
      "livemode": true,
      "data": { ... }
    }
  ],
  "next_cursor": "eyJ0c...",
  "has_more": true
}
```

`events` payloads are **identical** to the webhook envelope
(minus the signature — polling authenticates via your API key,
not per-delivery HMAC). Dedupe on `event_id` whether you're
receiving via webhook or poll; you'll use the same key for both.

### Polling best practices

- **Persist the cursor.** Save `next_cursor` after each successful
  batch to your own DB / disk. On restart, resume from it.
- **Dedupe on `event_id`.** Crash windows can cause you to
  re-process the last batch; same dedupe path you'd use for
  webhooks covers this.
- **Respect `X-Poll-Recommended-Interval`.** It's `ceil(3600 / limit)` —
  sleeping that long between polls guarantees you never hit the
  hour ceiling. Starter = 360 s, Pro = 60 s, Business = 6 s.
- **On 429, wait until `details.reset_at`.** Don't exponential-backoff —
  the window resets deterministically on the hour boundary. See
  [/docs/rate-limits](https://igregulator.io/docs/rate-limits/).
- **Hybrid webhooks + polling is the resilient pattern.** Webhooks
  for low latency, polling for the few-hour windows where your
  receiver was down and deliveries abandoned. Both ship the same
  `event_id`, so dedupe makes double-processing a no-op. Example
  in [/docs/webhooks § Pattern C](https://igregulator.io/docs/webhooks/#7-integration-patterns).

### `since=` is capped at 30 days

That matches our event retention. Older values fail with
`400 invalid_query` and `details.reason: "since_exceeds_retention_window"`.
Rare in practice — the first call uses `since`, every subsequent call
uses the cursor.

## 4. Plan limits in detail

Once your watchlist is at its cap, the next `POST
/v1/watchlist/operators` returns `403 quota_exceeded` with
`details.reason: "watchlist_quota_exceeded"` and the current count +
cap (`current_usage`, `limit`). Remove an operator, or email
founder@igregulator.io if you need a higher cap.

Polling ceiling hits return `429 rate_limited` with
`details.reason: "watchlist_events_poll_limit"` and a concrete
`details.reset_at` timestamp — not exponential backoff, deterministic
hour-boundary reset. Webhooks do not count against this limit. Error
reference: [errors](https://igregulator.io/docs/errors/).

## 5. What events don't fire for watched operators

- **`coverage.degraded` / `coverage.restored`** — these are
  jurisdiction-level, not operator-level. They're emitted
  regardless of watchlist membership; anyone subscribed to the
  event type gets them.
- **`webhook.endpoint_degraded`** — reserved for a self-notification
  about your own endpoints; **not sent yet**. It would ignore the
  watchlist.

## 6. Troubleshooting

- **No events arriving** — your watchlist may be empty, your webhook
  endpoint may not subscribe to the event types, or the operators you
  track haven't had any changes this month. Run
  `GET /v1/watchlist/events` with a `since` up to 30 days back to see
  what fired for your watchlist.
- **Too many events** — too-broad subscription. Each of the three
  expiry windows fires separately; narrow to the one(s) you act
  on.
- **Events for operators I don't watch** — your webhook might
  have `watchlist_only: false`. Check `PATCH /v1/webhooks/:id`
  with `{ "watchlist_only": true }`.

Related: [/docs/webhooks](https://igregulator.io/docs/webhooks/),
[/docs/rate-limits](https://igregulator.io/docs/rate-limits/).
