# Get told when a seller changes

Subscribe a signed webhook to observed price, recipient and liveness changes for the hosts you depend on. Admission key required.

Canonical: https://402signal.com/developers/alerts

## Get told when a seller you depend on changes

A subscription names up to 20 seller hosts and one public HTTPS webhook. Every two minutes the service compares each host's latest public observations with your subscription and, when something moved, delivers one signed batch of `price_changed`, `recipient_changed` and `liveness_changed` events. Alerts fire on the same observations the [endpoint pages](/endpoints) count: a change is reported when a check observed it, never from a catalog feed alone. An admission key (`X-402Signal-Key`) is required; ask at [ross@402signal.com](mailto:ross@402signal.com?subject=admission%20key).

### Subscribe

```
curl -sS -X POST https://402signal.com/alerts \
  -H "X-402Signal-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"url":"https://hooks.example.com/402signal","hosts":["api.example.com"],"events":["price","recipient","liveness"]}'
```

HTTP 201 returns the subscription `id`, `hosts_known` (whether each host has listings today) and `signing_secret`, shown once. The URL must be public HTTPS with public DNS: private, loopback and link-local addresses, plain HTTP, credentials in the URL and 402signal.com itself are refused at creation and again on every delivery. Up to 10 subscriptions per key, 20 hosts each; `events` defaults to all three.

### What you receive

One POST per scan when something changed. Headers: `X-402Signal-Event` (`402signal.alerts`, or `402signal.ping` for a test), `X-402Signal-Delivery` (the delivery id, also in the body) and `X-402Signal-Signature` (`t=<unix seconds>,v1=<hex HMAC-SHA256>` over `<t>.<raw body>`).

```
{"type": "402signal.alerts", "delivery_id": "9f1c2d3e4a5b6c7d", "subscription_id": "0123456789abcdef",
 "generated_at": "2026-09-13T18:20:00Z",
 "events": [
  {"event": "price_changed", "host": "api.example.com", "url": "https://api.example.com/v1/quote",
   "amount_atomic": "20000", "changed_at": "2026-09-13T18:19:41Z", "endpoint_page": "https://402signal.com/endpoints/api.example.com"},
  {"event": "recipient_changed", "host": "api.example.com", "url": "https://api.example.com/v1/quote",
   "payTo": "0xabc...", "observed_payTo": "0xdef...", "changed_at": "2026-09-13T18:19:41Z", "endpoint_page": "..."},
  {"event": "liveness_changed", "host": "api.example.com", "url": "https://api.example.com/v1/quote",
   "live": false, "miss_reason": "timeout", "observed_at": "2026-09-13T18:19:52Z", "endpoint_page": "..."}
 ]}
```

`price_changed` carries the new observed atomic amount on the same asset. `recipient_changed` carries the address on record and the newly observed one; it stays pending until a second observation confirms it, with no second alert for the confirmation. `liveness_changed` reports the latest observation flipping between answering with a valid challenge and not. Delivery is at least once: the same `event`, `url` and `changed_at` seen twice is the same change twice. Answer with any 2xx within five seconds; a 3xx is not followed and counts as a failure.

### Verify the signature

```
import hmac, hashlib, time

def verify(secret: str, header: str, body: bytes, tolerance_s: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    ts = int(parts.get("t", "0"))
    if abs(int(time.time()) - ts) > tolerance_s:
        return False
    expected = hmac.new(secret.encode(), b"%d." % ts + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))
```

### Failures and management

A delivery that raises, times out or answers outside 2xx counts as a failure. Retries back off from one minute to one hour and the undelivered changes stay owed, so the next successful delivery carries them. After 20 consecutive failures the subscription is disabled and shows `active: false` with the reason; a successful test ping re-enables it.

 | Call | Result

 | GET /alerts | Your subscriptions, no secrets, and the limits.

 | GET /alerts/<id> | One subscription with its last 20 deliveries: time, kind, HTTP status, event count, error class.

 | POST /alerts/<id>/test | A signed `402signal.ping` now; `delivered: true` on a 2xx.

 | DELETE /alerts/<id> | Removes it, HTTP 204.

Every call answers only for the key that created the subscription. The signing secret is stored on the private writer volume beside the session store; to rotate it, delete and recreate the subscription. Alerts are not uptime monitoring: a host nobody checks stays silent. Check your own key and credits any time with `GET /keys/usage`.
