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

# Webhook events

> Payload format and signature verification for monitoring webhooks

CompliAPI POSTs [monitoring webhook](/webhooks) events as JSON to your configured endpoints. This page documents the wire format; configuration happens on the dashboard's Webhooks page.

## Request

```
POST <your endpoint URL>
Content-Type: application/json
User-Agent: CompliAPI-Webhooks/1.0
X-CompliAPI-Signature: t=1756640000,v1=5f8c2a...
```

Answer any **2xx** within 10 seconds to acknowledge. Other responses (and timeouts) are retried — see [delivery and retries](/webhooks#delivery-and-retries).

## Payload

```json theme={null}
{
  "id": "evt_1234",
  "type": "entity.listed",
  "created_at": "2026-08-31T12:00:00Z",
  "monitored_entity": {
    "id": 42,
    "entity_type": "onchain_address",
    "value": "vitalik.eth",
    "label": "customer hot wallet"
  },
  "listing": {
    "list": "ofac",
    "list_name": "US OFAC SDN",
    "list_type": "sanctions",
    "value": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045",
    "source_url": "https://sanctionssearch.ofac.treas.gov/Details.aspx?id=...",
    "metadata": {"sdn_name": "EXAMPLE ENTITY", "programs": "DPRK3"}
  }
}
```

| Field               | Meaning                                                                                                                                                                                             |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                | Stable event id — identical across retries; deduplicate on it. Test events use `evt_test_<uuid>`.                                                                                                   |
| `type`              | `entity.listed`, `entity.delisted`, `entity.relisted`, or `test`                                                                                                                                    |
| `created_at`        | When the list change was ingested (ISO 8601, UTC)                                                                                                                                                   |
| `monitored_entity`  | Your monitored value as configured: `entity_type`, `value` (as you entered it — an ENS name stays a name), and your `label`. `id` is null on test events and after the monitored entity is deleted. |
| `listing.list`      | Source list slug — same registry as [`GET /screen/lists`](/api/screen/lists)                                                                                                                        |
| `listing.list_type` | `sanctions`, `crime`, or `risk` — same semantics as screening: only a sanctions-list change means a sanctions status change                                                                         |
| `listing.value`     | The value as published on the list (e.g. the resolved address)                                                                                                                                      |
| `listing.metadata`  | The list entry's attributes, same shape as a [screening match](/api/screen)                                                                                                                         |

## Verifying the signature

The `X-CompliAPI-Signature` header is `t=<unix timestamp>,v1=<hex HMAC-SHA256>`, where the MAC is computed over `"{t}." + <raw request body>` with your endpoint's signing secret (`whsec_...`, from the dashboard). Verify with a constant-time comparison and reject stale timestamps (we recommend a 5-minute tolerance) to prevent replays.

<CodeGroup>
  ```python Python theme={null}
  import hashlib, hmac, time

  def verify(secret: str, body: bytes, header: str, tolerance: int = 300) -> bool:
      parts = dict(p.split("=", 1) for p in header.split(","))
      timestamp, signature = parts.get("t", "0"), parts.get("v1", "")
      if abs(time.time() - int(timestamp)) > tolerance:
          return False
      expected = hmac.new(
          secret.encode(), f"{timestamp}.".encode() + body, hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(expected, signature)

  # In your handler (e.g. FastAPI):
  # verify(WEBHOOK_SECRET, await request.body(), request.headers["X-CompliAPI-Signature"])
  ```

  ```javascript Node theme={null}
  const crypto = require("crypto");

  function verify(secret, rawBody, header, tolerance = 300) {
    const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
    const timestamp = Number(parts.t);
    if (Math.abs(Date.now() / 1000 - timestamp) > tolerance) return false;
    const expected = crypto
      .createHmac("sha256", secret)
      .update(`${parts.t}.`)
      .update(rawBody) // the raw body Buffer — not re-serialized JSON
      .digest("hex");
    return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ""));
  }
  ```
</CodeGroup>

Verify against the **raw request body bytes**. Parsing the JSON and re-serializing it will produce different bytes and a false mismatch.

Rotating the secret from the dashboard invalidates the previous secret immediately — update your consumer first, then rotate.
