Webhook Signature Verification

VeriSwarm signs every webhook delivery with HMAC-SHA256 so you can verify the payload was not tampered with in transit.

Headers

Every webhook delivery includes:

Header Description
X-VeriSwarm-Signature HMAC-SHA256 hex digest of the request body
X-VeriSwarm-Timestamp Unix timestamp (seconds) when the webhook was sent
X-VeriSwarm-Delivery-Id Unique delivery ID for idempotency

Verification Steps

  1. Read the X-VeriSwarm-Signature and X-VeriSwarm-Timestamp headers
  2. Concatenate {timestamp}.{raw_body} (the timestamp, a dot, and the raw request body bytes)
  3. Compute HMAC-SHA256 using your webhook signing secret as the key
  4. Compare the computed hex digest with the signature header (use constant-time comparison)
  5. Optionally check the timestamp is within 5 minutes to prevent replay attacks

Python Example

import hmac
import hashlib
import time

def verify_webhook(raw_body: bytes, signature: str, timestamp: str, secret: str) -> bool:
    # Check timestamp freshness (optional but recommended)
    if abs(time.time() - int(timestamp)) > 300:
        return False  # Older than 5 minutes

    # Compute expected signature
    message = f"{timestamp}.{raw_body.decode('utf-8')}".encode("utf-8")
    expected = hmac.new(
        key=secret.encode("utf-8"),
        msg=message,
        digestmod=hashlib.sha256,
    ).hexdigest()

    # Constant-time comparison
    return hmac.compare_digest(expected, signature)


# Usage in Flask
@app.route("/webhooks/veriswarm", methods=["POST"])
def handle_veriswarm_webhook():
    raw_body = request.get_data()
    signature = request.headers.get("X-VeriSwarm-Signature", "")
    timestamp = request.headers.get("X-VeriSwarm-Timestamp", "")

    if not verify_webhook(raw_body, signature, timestamp, WEBHOOK_SECRET):
        return "Invalid signature", 401

    payload = request.get_json()
    # Process the event...
    return "OK", 200

Node.js Example

const crypto = require("crypto");

function verifyWebhook(rawBody, signature, timestamp, secret) {
  // Check timestamp freshness
  if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) {
    return false;
  }

  const message = `${timestamp}.${rawBody}`;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(message)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(signature, "hex")
  );
}

// Usage in Express
app.post("/webhooks/veriswarm", express.raw({ type: "*/*" }), (req, res) => {
  const rawBody = req.body.toString();
  const signature = req.headers["x-veriswarm-signature"] || "";
  const timestamp = req.headers["x-veriswarm-timestamp"] || "";

  if (!verifyWebhook(rawBody, signature, timestamp, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send("Invalid signature");
  }

  const payload = JSON.parse(rawBody);
  // Process the event...
  res.status(200).send("OK");
});

Webhook Event Types

Event Description
decision.checked A trust decision was made (includes agent_id, decision, reason_code)
provider.report.ingested An event/report was ingested
agent.killed An agent's kill switch was activated (Guard)
agent.verified An agent's identity was verified (Passport)

Testing

Use the webhook test endpoint to send a test delivery to your configured URL:

curl -X POST https://api.veriswarm.ai/v1/public/providers/webhooks/{webhook_id}/test \
  -H "x-account-access-token: YOUR_TOKEN"

Retry Policy

Failed deliveries are retried with exponential backoff:

  • Attempt 1: Immediate
  • Attempt 2: 30 seconds
  • Attempt 3: 2 minutes
  • Attempt 4: 5 minutes
  • Final: Marked as exhausted, alert sent to admin