Signature verification
Every webhook carries an HMAC-SHA256 signature in the
X-AIActRadar-Signature header, computed over the raw request
body using your subscription's webhook_secret. Verify it
before parsing or trusting the payload.
Always compare in constant time. Naive equality leaks timing information that lets an attacker brute-force the signature.
Format
X-AIActRadar-Signature: sha256=3f8a2c1d… Strip the sha256= prefix, then compare against
HMAC-SHA256(webhook_secret, raw_request_body).hex().
Python
import hmac, hashlib
def verify_signature(secret: str, payload: bytes, header: str) -> bool:
if not header.startswith("sha256="):
return False
received = header[len("sha256="):]
expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, received) Node.js
import { createHmac, timingSafeEqual } from 'node:crypto';
function verifySignature(secret, payload, header) {
if (!header?.startsWith('sha256=')) return false;
const received = header.slice('sha256='.length);
const expected = createHmac('sha256', secret).update(payload).digest('hex');
if (received.length !== expected.length) return false;
return timingSafeEqual(Buffer.from(received), Buffer.from(expected));
} Go
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strings"
)
func verifySignature(secret string, payload []byte, header string) bool {
if !strings.HasPrefix(header, "sha256=") {
return false
}
received, err := hex.DecodeString(strings.TrimPrefix(header, "sha256="))
if err != nil {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(payload)
return hmac.Equal(received, mac.Sum(nil))
} Common mistakes
- Re-serialising before verifying — sign over the raw bytes you received, not the JSON your framework re-stringified.
- Forgetting to strip the prefix — the header is
sha256=<hex>, not just<hex>. - Comparing as strings — use a constant-time compare.
- Confusing the secret — use the per-subscription
webhook_secret, not your API key.
Lost the secret?
Email [email protected] to rotate. We delete the existing subscription and create a new one — your webhook URL stays the same, the secret changes.