# Watches, events and webhooks

A watch tells you when a domain, phone number, crypto wallet or brand changes: a domain moves to new IP addresses or name servers, goes down, is held or expires, a number or wallet is listed, or a new lookalike of a brand is registered. Events are kept for 13 months at GET /v1/events and pushed to signed webhooks. A watch costs 1 credit a day and needs an API key.

Updated 2026-10-30.

## What you can watch

| Entity | `type` | Events |
|---|---|---|
| A domain | `domain` | `domain.ip_changed`, `domain.ns_changed`, `domain.down`, `domain.up`, `domain.parked`, `domain.held`, `domain.reregistered`, `domain.expiring`, `domain.expired`, `domain.listed`, `domain.delisted` |
| A phone number | `phone` | `phone.listed`, `phone.delisted` |
| A crypto wallet | `wallet` | `wallet.listed`, `wallet.delisted`, `wallet.sanctioned` |
| A brand | `brand` | `brand.lookalike_registered`, `brand.lookalike_cert`, `brand.lookalike_live` |

A domain's events come from Bewary's hourly DNS re-check, the daily registry pass and the scam lists. A brand's come from the certificate logs and the registries' zone files: a new name that imitates the brand, as soon as it is registered, gets a certificate or starts answering.

## Start a watch

```bash
curl https://api.bewary.app/v1/watches \
  -H "Authorization: Bearer $BEWARY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"entity": {"type": "domain", "value": "paypal-login-secure.top"}, "events": ["domain.down", "domain.ip_changed"]}'
```

Leave out `events` to get every event of the entity's type. Watching an entity you already watch replaces its event types and charges nothing. An account holds up to 1,000 live watches. `GET /v1/watches` lists them, `GET /v1/watches/{id}` shows one with its last 30 days of events and deliveries, and `DELETE /v1/watches/{id}` stops it. Its events stay readable.

## Read events

`GET /v1/events` is the durable record, oldest first, kept for 13 months. Store `next_cursor` and pass it back as `cursor` to read what came since. It is returned even when no event came, so a poller keeps its place.

```bash
curl "https://api.bewary.app/v1/events?cursor=$CURSOR&types=domain.down,domain.ip_changed" \
  -H "Authorization: Bearer $BEWARY_API_KEY"
```

Each event carries `before` and `after` with only the fields the change is about, such as the old and new IP addresses, and the datasets it was read from.

## Webhooks

`POST /v1/webhooks` with an `https` URL registers an endpoint and returns its signing secret once. An account holds up to 5 endpoints. Each event is posted with the [Standard Webhooks](https://www.standardwebhooks.com/) headers `webhook-id` (the event id, the same on every retry), `webhook-timestamp` and `webhook-signature`. Answer with a 2xx within 15 seconds. A failed delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours, and an endpoint that fails for 5 days is disabled. `POST /v1/webhooks/{id}/rotate` issues a new secret, and the old one keeps signing for 24 hours.

Verify every delivery before you trust it:

```python
import base64, hashlib, hmac, time

def verify(secret: str, headers: dict, body: bytes) -> bool:
    key = base64.b64decode(secret.removeprefix("whsec_"))
    ts = int(headers["webhook-timestamp"])
    if abs(time.time() - ts) > 300:                       # a replayed delivery
        return False
    signed = f'{headers["webhook-id"]}.{ts}.'.encode() + body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
    return any(hmac.compare_digest(sig.split(",", 1)[1], expected)
               for sig in headers["webhook-signature"].split() if sig.startswith("v1,"))
```

```typescript
import { createHmac, timingSafeEqual } from "node:crypto"

export function verify(secret: string, headers: Record<string, string>, body: Buffer): boolean {
  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64")
  const ts = Number(headers["webhook-timestamp"])
  if (Math.abs(Date.now() / 1000 - ts) > 300) return false   // a replayed delivery
  const expected = createHmac("sha256", key).update(`${headers["webhook-id"]}.${ts}.`).update(body).digest()
  return headers["webhook-signature"].split(" ").some((s) => {
    const [v, sig] = s.split(",")
    const got = Buffer.from(sig ?? "", "base64")
    return v === "v1" && got.length === expected.length && timingSafeEqual(got, expected)
  })
}
```

## Price

A watch costs 1 credit a day, charged a day ahead. A brand watch also costs 1 credit for each lookalike event. When the balance cannot pay the next day, the watch pauses, and a top-up charges the missed days and produces the events of the gap. Reading events and managing webhooks are free. Watches need an API key.

## From an agent

The MCP server has `watch`, `unwatch`, `list_watches` and `list_events`, and the CLI has `bewary watch`, `bewary watches`, `bewary unwatch` and `bewary events`. Webhook endpoints are managed through the REST API only.
