Webhook signatures
When you configure a webhook channel with a secret, IMAA signs every outbound request so your handler can verify it actually came from us. The scheme is the same one Stripe uses: HMAC-SHA256 over a timestamp-prefixed payload.
Request format
The body is JSON with three fields: event (the alert subject line),body (a multi-line text description), and timestamp (unix seconds).
{
"event": "[IMAA Alert] High TX Rate — WARNING",
"body": "Contract: USDT (0xdAC1...)\nRule: High TX Rate\nMetric (tx_rate): 27\nThreshold: 10\nBlock range: 19123456-19123466",
"timestamp": "1776384000"
}Headers on every request:
X-IMAA-Signature: sha256=<hex>X-IMAA-Timestamp: <unix seconds>Content-Type: application/jsonUser-Agent: IMAA-Webhook/1.0
Delivery has a 10-second timeout. Failed deliveries (non-2xx, timeout, connection error) are retried up to 3 times with exponential backoff.
Signature scheme
Compute HMAC-SHA256 over the bytes {timestamp}.{raw_body} using your configured secret. Compare the resulting hex digest against the value after sha256= in X-IMAA-Signature using a constant-time compare.
Critically: hash the raw request body bytes, not a re-serialized object. JSON whitespace and key order matter.
Verifying in Node.js
import crypto from "node:crypto";
export function verify(rawBody: Buffer, headers: Record<string, string>, secret: string) {
const ts = headers["x-imaa-timestamp"];
const sig = headers["x-imaa-signature"];
if (!ts || !sig) return false;
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${ts}.`)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(sig.replace("sha256=", "")), Buffer.from(expected));
}Verifying in Python
import hmac, hashlib, time
def verify(raw_body: bytes, headers: dict, secret: str) -> bool:
ts = headers.get("x-imaa-timestamp")
sig = headers.get("x-imaa-signature", "")
if not ts or not sig.startswith("sha256="):
return False
if abs(time.time() - int(ts)) > 300:
return False
mac = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256)
return hmac.compare_digest(sig.removeprefix("sha256="), mac.hexdigest())Replay protection
Reject any request where |now − X-IMAA-Timestamp| > 300 seconds. This bounds how long a captured request stays valid if an attacker ever gets hold of one. Five minutes is enough slack for normal clock drift without leaving a meaningful replay window.