> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aegis-kyt.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks overview

> Push notifications for Aegis events — exposure jobs, high-risk hits, KYT policy alerts.  HMAC-signed, retried, auto-disabled on extended failure.

<Note>
  **Two surfaces, same deliveries:**

  * **API:** `https://api.aegis-kyt.com/v1/webhooks/*` · `X-API-Key: aeg_<48 chars>` — for x402 consumers / integrations without a UI session
  * **Dashboard:** `https://app.aegis-kyt.com/webhooks` — for VASP / PSP customers using the web app; scoped to your subgroup
</Note>

Webhooks let your service receive Aegis events as they happen instead
of polling.  Every delivery is a **POST** of a JSON body to a URL you
register, signed with HMAC-SHA256 using a secret we mint once at
subscription creation.

Subscriptions registered through either surface receive the same
event types with identical payload shapes.  See
[Dashboard self-service](/webhooks/dashboard) for the web UI flow or
[Manage subscriptions](/webhooks/manage) for the REST API flow.

## When you'd use webhooks

| Event type                  | Fires when                                                                                                           | Useful for                                                |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| `tx.policy.alert.triggered` | A `/check-transfer` or `/v1/aegis/tx` check raises at least one policy alert above your configured `alert_min_level` | KYT — funnel inbound deposits into your case-review queue |
| `aegis.bfs.completed`       | An asynchronous Tier-4 exposure job finishes                                                                         | Long-running BFS pre-screening, batch flows               |
| `aegis.bfs.failed`          | Same job gives up after retries                                                                                      | Surface the failure into your ops queue                   |
| `aegis.check.high_risk`     | A `/v1/aegis/check` returns risk\_level `high` or `severe`                                                           | Real-time alerting on standalone address checks           |

You can subscribe to any non-empty subset; one subscription URL can
listen to multiple event types.

## Delivery contract

Every delivery is a `POST` to your URL with these headers:

| Header                | Value                                                              |
| --------------------- | ------------------------------------------------------------------ |
| `Content-Type`        | `application/json`                                                 |
| `User-Agent`          | `aegis-webhooks/1`                                                 |
| `X-Aegis-Event`       | The event\_type string, e.g. `tx.policy.alert.triggered`           |
| `X-Aegis-Signature`   | Base64-encoded `HMAC_SHA256(secret, raw_body)`                     |
| `X-Aegis-Delivery-Id` | UUID of this delivery attempt — survives retries (idempotency key) |
| `X-Aegis-Attempt`     | 1-based attempt counter (1 on first try, 2 on first retry, etc.)   |

Your endpoint should:

1. **Verify the signature** against the *raw* request body (don't
   parse + re-serialize — JSON whitespace matters).
2. **Respond `2xx` within 15 s** — anything else is retried.
3. **Deduplicate by `X-Aegis-Delivery-Id`** if you store events —
   we retry on any non-2xx, so the same delivery\_id may arrive
   multiple times during transient outages.

### Signature verification

The signature is computed over the **exact bytes** of the POST body:

```python theme={null}
import hmac, hashlib, base64

def verify(secret: str, body_bytes: bytes, signature: str) -> bool:
    expected = base64.b64encode(
        hmac.new(secret.encode(), body_bytes, hashlib.sha256).digest()
    ).decode()
    return hmac.compare_digest(expected, signature)
```

```javascript theme={null}
import crypto from "node:crypto";

function verify(secret, bodyBuf, signature) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(bodyBuf)
    .digest("base64");
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature),
  );
}
```

Use **constant-time comparison** (`hmac.compare_digest` /
`crypto.timingSafeEqual`) — direct `===` leaks timing.

### Retry policy

Delivery attempts on non-2xx response or network error:

| Attempt | Delay     |
| ------- | --------- |
| 1       | immediate |
| 2       | +30 s     |
| 3       | +2 min    |
| 4       | +10 min   |
| 5       | +1 h      |

After 5 failed attempts we mark the delivery `gave_up`.  If a
subscription accumulates `failure_threshold` consecutive
`gave_up` deliveries (default `5`, configurable per subscription),
we **auto-disable** the subscription and send a Telegram alert
to the operator channel.  Re-enable via `PUT /v1/webhooks/{id}`
with `{is_active: true}` — that also resets the failure counter.

## Synthetic test event

Use `POST /v1/webhooks/{sub_id}/test` to send a synthetic event
through the same signature + retry plumbing real events use.
Payloads carry sentinel ids (`00000000-…`) and a top-level
`synthetic: true` field so your handler can short-circuit them
if desired.  See [/v1/webhooks/{id}/test](/webhooks/test-endpoint).

## What you don't get

* **No queueing on your side** — if your endpoint is down for >1 h
  (sum of retry window) we give up on the individual delivery.
  Critical events should still be polled defensively.
* **No batching** — one HTTP call per event, per subscription.
* **No filtering beyond event\_type + your `subgroup_aml_config`
  alert\_min\_level** — for `tx.policy.alert.triggered`, see
  [event schemas](/webhooks/events) for the body fields you
  can route on.

## Event schemas

See [/webhooks/events](/webhooks/events) for the full body shape
of every event type.
