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/json
  • User-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.