Live

Reference

Webhooks — real-time alert delivery

Configure HMAC-signed webhook delivery for alerts on the wallets you follow. Includes the payload schema, signature verification, and Node + Python examples.

Last updated

Webhooks push a signed JSON payload to your server the moment an alert fires on a wallet you follow. They're a Pro feature — configure them under Settings → Webhooks.

When a webhook fires

A delivery is sent whenever a whale or insider alert is recorded for a wallet you follow — directly or through a list you follow. You receive only alerts for wallets in your follow set; there is no global firehose.

Payload

Every delivery is a POST with a JSON body shaped like this:

{
  "event": "alert.followed_wallet",
  "alert": {
    "type": "insider",
    "id": 12345,
    "score": 82,
    "betValue": 25000,
    "side": "BUY",
    "outcome": "Yes",
    "price": 0.42,
    "explanation": "High win rate at uncertain odds…",
    "createdAt": "2026-05-27T12:00:00.000Z"
  },
  "wallet": {
    "address": "0x1234…5678",
    "type": "insider",
    "profileUrl": "https://crowdintel.xyz/whales/0x1234…5678"
  },
  "market": {
    "title": "Will X happen by 2026?",
    "category": "politics",
    "slug": "will-x-happen-by-2026"
  },
  "followedVia": "wallet"
}

followedVia is "wallet" for a direct follow or "list:<slug>" when the wallet came from a list you follow.

Headers

HeaderDescription
X-CrowdIntel-SignatureHMAC-SHA256 of ${timestamp}.${rawBody}, hex-encoded, signed with your endpoint secret.
X-CrowdIntel-TimestampUnix epoch milliseconds at send time. Reject requests whose timestamp is too old to block replays.
User-AgentCrowdIntel-Webhooks/1

Verifying the signature

Compute the HMAC over the exact raw request body prefixed with the timestamp and a dot (${timestamp}.${body}), then compare it to X-CrowdIntel-Signature in constant time. Your secret is shown once when you create the endpoint (prefix whsec_).

Node.js

import crypto from "node:crypto";

function verify(rawBody, signature, timestamp, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");
  const ok =
    signature.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
  // Reject deliveries older than 5 minutes (replay protection).
  const fresh = Math.abs(Date.now() - Number(timestamp)) < 5 * 60 * 1000;
  return ok && fresh;
}

// Express: capture the RAW body so the bytes match what we signed.
app.post("/webhooks/crowdintel", express.raw({ type: "application/json" }), (req, res) => {
  const rawBody = req.body.toString("utf8");
  const valid = verify(
    rawBody,
    req.header("X-CrowdIntel-Signature"),
    req.header("X-CrowdIntel-Timestamp"),
    process.env.CROWDINTEL_WEBHOOK_SECRET,
  );
  if (!valid) return res.status(401).end();
  const payload = JSON.parse(rawBody);
  // … handle payload …
  res.status(200).end();
});

Python

import hashlib
import hmac
import time

def verify(raw_body: bytes, signature: str, timestamp: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        f"{timestamp}.".encode() + raw_body,
        hashlib.sha256,
    ).hexdigest()
    if not hmac.compare_digest(expected, signature):
        return False
    # Reject deliveries older than 5 minutes (replay protection).
    return abs(time.time() * 1000 - int(timestamp)) < 5 * 60 * 1000

# Flask
@app.post("/webhooks/crowdintel")
def crowdintel_webhook():
    raw = request.get_data()  # raw bytes, before JSON parsing
    ok = verify(
        raw,
        request.headers.get("X-CrowdIntel-Signature", ""),
        request.headers.get("X-CrowdIntel-Timestamp", ""),
        os.environ["CROWDINTEL_WEBHOOK_SECRET"],
    )
    if not ok:
        abort(401)
    payload = request.get_json()
    # … handle payload …
    return "", 200

Responses and retries

Return a 2xx status to acknowledge a delivery. Any non-2xx response (or a timeout — we wait 10 seconds) is retried with exponential backoff: 5s, then 30s, then 5min. After the fourth failed attempt the delivery is marked dead and surfaced in your recent deliveries list. Successful and dead deliveries are not retried.

Endpoints should be idempotent: a given alert is delivered at most once per endpoint, but design your handler to tolerate an occasional duplicate.

Testing

Use the Test button on Settings → Webhooks to send a sample payload to your endpoint and confirm your signature verification works before a real alert fires.

Live
Beta
···v0.1.0

CrowdIntel needs a newer browser

Your device is on iOS 15 or older. CrowdIntel uses features (color-mix, WebGL2, modern auth) that require iOS 16+ / Safari 16+.

iPhone 7 maxes out at iOS 15, so the site can't render here. Open CrowdIntel on a desktop browser or a newer phone.

crowdintel.xyz

◆ legacy browser fallback