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.
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
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.
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 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:
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,"))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.
Updated · This page as Markdown · Changelog