# Webhooks

> Signed run.* and dataset.updated events — Standard Webhooks verification in 10 lines


Register an endpoint and DeepSieve POSTs you events instead of making you poll:

```bash
curl -s -X POST $BASE_URL/v1/webhooks \
  -H "Authorization: Bearer $DEEPSIEVE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://your-app.example.com/hooks/deepsieve",
       "events": ["run.completed", "run.failed", "dataset.updated"]}'
```

The response includes `"secret": "whsec_..."` — shown once; use it to verify
signatures. Endpoints must be public HTTPS; up to 10 active endpoints per workspace.

## Events

| Event | When |
|---|---|
| `run.completed` / `run.failed` / `run.cancelled` | a research run reaches a terminal state |
| `dataset.updated` | a monitored value was re-verified and **changed** (payload: table, record_id, column, new + superseded evidence ids) |

## Verify signatures (Standard Webhooks)

Headers on every delivery: `webhook-id`, `webhook-timestamp` (unix seconds),
`webhook-signature` (`v1,<base64 HMAC-SHA256>` over `"{id}.{timestamp}.{body}"`).
Any Standard Webhooks library works, or by hand:

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

def verify(secret: str, headers: dict, body: bytes) -> bool:
    ts = headers["webhook-timestamp"]
    if abs(time.time() - int(ts)) > 300:   # ±5 min tolerance
        return False
    msg = f"{headers['webhook-id']}.{ts}.".encode() + body
    mac = hmac.new(secret.encode(), msg, hashlib.sha256)
    expected = "v1," + base64.b64encode(mac.digest()).decode()
    return hmac.compare_digest(expected, headers["webhook-signature"])
```

```typescript
import crypto from "node:crypto";

function verify(secret: string, headers: Record<string, string>, body: string): boolean {
  const ts = headers["webhook-timestamp"];
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const mac = crypto.createHmac("sha256", secret)
    .update(`${headers["webhook-id"]}.${ts}.${body}`).digest("base64");
  return crypto.timingSafeEqual(Buffer.from(`v1,${mac}`),
    Buffer.from(headers["webhook-signature"]));
}
```

## Delivery & retries

2xx acknowledges. Anything else retries with backoff: 1m, 5m, 30m, 2h, 12h,
then the delivery is dropped. `webhook-id` is stable across retries — use it to
deduplicate. Test with `POST /v1/webhooks/{id}/ping`. Manage with
`GET`/`PATCH`/`DELETE /v1/webhooks/{id}` (PATCH keeps the secret; `DELETE`
disables). Rotate a leaked secret with `POST /v1/webhooks/{id}/rotate` — it
returns a new `whsec_` once and the old secret stops signing **immediately**, so
re-point your verifier first. Bring a disabled endpoint back with
`POST /v1/webhooks/{id}/enable` (the secret is unchanged).
