Back to Docs

Documentation

Webhooks

Pro

Webhooks deliver watchlist alerts (rating changes, grade transitions, new ratings) to a URL you control. Pro tier and above. HMAC-SHA256 signed; retried with exponential backoff on non-2xx responses.

Configuring a webhook

Webhooks are configured per-watchlist on /dashboard/watchlist. Each watchlist can have one webhook destination URL plus a signing key that Verdict generates for you.

Coming soon

The "reveal signing key" UI on the dashboard ships in B2. Until then, request your signing key from support@verdict.finance when you set up your first watchlist webhook. The webhook delivery infrastructure itself is live — alerts will fire as soon as the watchlist is configured.

Payload format

POST request, JSON body, content-type application/json:

{
  "event": "rating.changed",
  "timestamp": "2026-04-28T14:23:11.000Z",
  "delivery_id": "del_abc123",
  "watchlist_id": "wl_xyz789",
  "data": {
    "entity_type": "protocol",
    "entity_slug": "aave",
    "previous": { "composite_score": 88.5, "letter_grade": "AA" },
    "current":  { "composite_score": 84.2, "letter_grade": "A" },
    "domain_changes": [
      { "domain": "security", "previous": 0.92, "current": 0.85 }
    ]
  }
}

Signature verification

Every webhook includes an X-Verdict-Signature header containing an HMAC-SHA256 hex digest of the raw request body using your watchlist's signing key. Always verify before trusting the payload, and use a constant-time comparison so you don't leak signature bytes via timing.

Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody: Buffer, signatureHeader: string, signingKey: string) {
  const expected = createHmac("sha256", signingKey).update(rawBody).digest();
  const provided = Buffer.from(signatureHeader, "hex");
  return (
    provided.length === expected.length &&
    timingSafeEqual(provided, expected)
  );
}
Python
import hmac
import hashlib

def verify(raw_body: bytes, signature_header: str, signing_key: str) -> bool:
    expected = hmac.new(
        signing_key.encode(),
        raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header)

Always verify against the raw request body — re-serializing the parsed JSON produces a different byte string and the signature will fail.

Retry behaviour

On non-2xx response (or no response within 10 seconds), Verdict retries with exponential backoff:

  • Retry 1: 30 seconds after the initial attempt
  • Retry 2: 5 minutes
  • Retry 3: 30 minutes
  • Retry 4: 2 hours
  • Retry 5: 12 hours (final attempt)

After 5 failed attempts the delivery is marked failed and surfaced on /dashboard/watchlist. Failed deliveries don't replay automatically — you can manually re-fire from the dashboard or via API.

Idempotency

Each delivery has a unique delivery_id. Your endpoint must be idempotent — the same delivery may arrive more than once if a 2xx response was lost in transit. Store the latest delivery_id per watchlist and ignore duplicates.

Testing locally

Use a tunnelling service (ngrok, Cloudflare Tunnel) to expose localhost for development. Verdict fires a test event when you save a webhook URL — the test event has event: "webhook.test" and a synthetic payload, so handle it the same way as a real delivery.