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

# Monitoring Webhooks

> Receive signed HTTPS notifications when on-chain activity happens on addresses you watch

## Overview

Monitoring webhooks push signed HTTPS notifications to your endpoint when on-chain activity happens on addresses you watch. Configure them in the [developer console](https://app.credprotocol.com/dashboard/webhooks): choose an endpoint URL, the event types you care about, the chains to watch, and up to 25 addresses per webhook.

Watched addresses are polled roughly every 5 minutes, so expect a delivery within a few minutes of the on-chain event.

## Event Types

| Event         | Fires when                                                              |
| ------------- | ----------------------------------------------------------------------- |
| `transfer`    | An ERC20 token moves to or from a watched address                       |
| `mint`        | A token is minted to a watched address (transfer from the zero address) |
| `burn`        | A token is burned from a watched address (transfer to the zero address) |
| `transaction` | Native currency (ETH, POL, BNB, ...) moves to or from a watched address |
| `approval`    | A watched address grants an ERC20 allowance to a spender                |

Protocol-level events (swaps, staking, liquidations) are not yet supported.

## Supported Chains

Ethereum, Base, Optimism, Arbitrum, Polygon, BNB Chain, Avalanche, Scroll, Linea, and Celo. Chain values in payloads use lowercase slugs (`ethereum`, `base`, `bsc`, ...).

## Endpoint Requirements

* **HTTPS only**, on port 443 or 8443, resolving to a public IP address (private and internal hosts are rejected, both at registration and at delivery time).
* Respond with any **2xx status within 10 seconds**. Anything else counts as a failed delivery.
* At most **10 webhooks per user** and **25 addresses per webhook**.

## Delivery Payload

Every delivery is a `POST` with a JSON body:

```json theme={null}
{
  "event_type": "transfer",
  "blockchain": "base",
  "address": "0xab5801a7d398351b8be11c439e05c5b3259aec9b",
  "transaction_hash": "0x9f1a...",
  "timestamp": "2026-08-20T17:04:11.482919+00:00",
  "data": {
    "from": "0x1234...",
    "to": "0xab58...",
    "direction": "in",
    "contract_address": "0x8335...",
    "token_symbol": "USDC",
    "token_decimal": "6",
    "value": "25000000",
    "block_number": 34118260
  }
}
```

The `data` object varies by event type: `transaction` events carry `value_wei`, `approval` events carry `spender` and the raw allowance `value`, and token events carry the fields shown above.

### Headers

| Header                  | Meaning                                                             |
| ----------------------- | ------------------------------------------------------------------- |
| `X-Webhook-Signature`   | `t=<unix_timestamp>,v1=<hex_hmac>` — see verification below         |
| `X-Event-Type`          | The event type, e.g. `transfer`                                     |
| `X-Webhook-Id`          | The webhook's ID                                                    |
| `X-Webhook-Delivery-Id` | Unique per delivery — use it for idempotency (retries reuse the ID) |
| `X-Webhook-Timestamp`   | Same unix timestamp as `t` in the signature header                  |

## Verifying Signatures

Each webhook has a signing secret, shown once when you create it. The signature is HMAC-SHA256 over the string `{t}.{raw_body}`, where `t` is the timestamp from the signature header and `raw_body` is the exact request body bytes. Always verify the signature **and** reject stale timestamps (5 minutes is a reasonable tolerance) to prevent replays.

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

  def verify_webhook(secret: str, signature_header: str, raw_body: bytes,
                     tolerance_seconds: int = 300) -> bool:
      parts = dict(p.split("=", 1) for p in signature_header.split(","))
      timestamp, signature = int(parts["t"]), parts["v1"]

      if abs(time.time() - timestamp) > tolerance_seconds:
          return False  # stale — possible replay

      expected = hmac.new(
          secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(expected, signature)
  ```

  ```typescript TypeScript theme={null}
  import { createHmac, timingSafeEqual } from "node:crypto";

  function verifyWebhook(
    secret: string,
    signatureHeader: string,
    rawBody: string,
    toleranceSeconds = 300,
  ): boolean {
    const parts = Object.fromEntries(
      signatureHeader.split(",").map((p) => p.split("=", 2) as [string, string]),
    );
    const timestamp = Number(parts.t);
    if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;

    const expected = createHmac("sha256", secret)
      .update(`${timestamp}.${rawBody}`)
      .digest("hex");
    return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
  }
  ```
</CodeGroup>

<Warning>
  Compute the HMAC over the **raw request body bytes**, not a re-serialized copy of the parsed JSON — serialization differences will break verification.
</Warning>

## Retries and Failure Handling

* Failed deliveries are retried up to 4 times with increasing backoff: **1 minute, 5 minutes, 30 minutes, then 2 hours**.
* Every attempt is recorded in the delivery history, visible in the console next to each webhook.
* After **10 consecutive failed attempts** (across deliveries), the webhook is automatically disabled. Re-enable it from the console once your endpoint is healthy — the failure counter resets on the first successful delivery.

## Testing

Use the **test button** in the console to send a synthetic `test` event through the real delivery pipeline. Test deliveries appear in the delivery history but are never retried and don't count toward auto-disable.

<Note>
  When a new address is added to a webhook, monitoring starts from the current chain head — historical activity is not backfilled.
</Note>
