Webhook events

The JSON Countermind sends to your webhook, and how to verify that it came from Countermind.

Delivery

  • One HTTPS POST per event, with Content-Type: application/json.
  • Your endpoint should answer with a 2xx status within 5 seconds.
  • Failed deliveries are not retried. The integration shows the last status, so you can spot problems.

Payloads

finding.published
{
  "event": "finding.published",
  "title": "Merchants can read each other's settlements",
  "publicId": "ZF-0001-01",
  "severity": "critical"
}
scan.completed
{
  "event": "scan.completed",
  "number": 5,
  "asset": "Merchant dashboard",
  "findings": 3
}
retest.published
{
  "event": "retest.published",
  "publicId": "ZF-0001-02",
  "result": "fixed"
}

result is fixed or not_fixed. Send test delivers a finding.published event with the ID ZF-TEST.

Verify the signature

When the webhook has a signing secret, each request carries an x-countermind-signature header: the lowercase hex HMAC-SHA256 of the raw request body, keyed with your secret. Compute it over the exact bytes you received and compare in constant time.

Node.js
import crypto from 'node:crypto';

export function isFromCountermind(rawBody, header, secret) {
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  const a = Buffer.from(expected), b = Buffer.from(header || '');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Python
import hashlib, hmac

def is_from_countermind(raw_body: bytes, header: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header or "")