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
| Header | Description |
|---|---|
X-CrowdIntel-Signature | HMAC-SHA256 of ${timestamp}.${rawBody}, hex-encoded, signed with your endpoint secret. |
X-CrowdIntel-Timestamp | Unix epoch milliseconds at send time. Reject requests whose timestamp is too old to block replays. |
User-Agent | CrowdIntel-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.