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

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.