Skip to content

Custom Webhook Integration

Receive Aegis Firma events in real time. Works with any language or framework. HMAC-SHA256 verified — secure from the start.

Quick start

Go to Dashboard → Integrations, click Add Webhook, enter your endpoint URL, and copy the signing secret. Then implement the verification code below.

Events

Aegis Firma fires these events to your endpoint:

EventDescription
task.completedA compliance task was marked complete
score.changedCompliance score changed by ≥2 points
regulation.newNew regulatory development detected
breach.detectedMonitoring gap or breach detected
vendor.risk_changedVendor risk score updated significantly
test.pingManual test from Integrations page

Payload format

Every webhook is a JSON POST with this structure:

{
  "event": "score.changed",
  "org_id": "org_abc123",
  "timestamp": "2026-04-19T10:30:00.000Z",
  "data": {
    "previous": 72,
    "current": 81,
    "delta": 9,
    "direction": "up"
  }
}

Signature verification

Every request includes an X-Aegis Firma-Signature header — an HMAC-SHA256 hex digest of the raw request body, signed with your webhook secret. Always verify this before processing the event.

Node.js
// Node.js + Express
const crypto = require('crypto')
const express = require('express')
const app = express()

app.use(express.json({ verify: (req, res, buf) => {
  req.rawBody = buf
}}))

app.post('/webhook', (req, res) => {
  const secret = process.env.COMPLIANCEIQ_WEBHOOK_SECRET
  const signature = req.headers['x-aegisfirma-signature']
  const expected = crypto
    .createHmac('sha256', secret)
    .update(req.rawBody)
    .digest('hex')

  if (!crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  )) {
    return res.status(401).send('Invalid signature')
  }

  const { event, data } = req.body
  console.log('Received event:', event, data)

  // Handle events
  switch (event) {
    case 'score.changed':
      if (data.direction === 'down') {
        // Alert your team
        sendSlackAlert(`Compliance score dropped: ${data.previous} → ${data.current}`)
      }
      break
    case 'regulation.new':
      // Log to your system
      saveRegulation(data)
      break
  }

  res.status(200).json({ received: true })
})
python
# Python + Flask
import hmac
import hashlib
from flask import Flask, request, abort
import os

app = Flask(__name__)

def verify_signature(payload: bytes, signature: str) -> bool:
    secret = os.environ['COMPLIANCEIQ_WEBHOOK_SECRET'].encode()
    expected = hmac.new(secret, payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

@app.route('/webhook', methods=['POST'])
def webhook():
    signature = request.headers.get('X-Aegis Firma-Signature', '')
    if not verify_signature(request.data, signature):
        abort(401)

    payload = request.get_json()
    event = payload['event']
    data = payload['data']

    print(f'Received event: {event}', data)

    if event == 'score.changed' and data['direction'] == 'down':
        send_alert(f"Compliance score dropped: {data['previous']} → {data['current']}")
    elif event == 'regulation.new':
        save_regulation(data)

    return {'received': True}, 200
php
<?php
// PHP (no framework)

$secret = $_ENV['COMPLIANCEIQ_WEBHOOK_SECRET'];
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_COMPLIANCEIQ_SIGNATURE'] ?? '';

$expected = hash_hmac('sha256', $payload, $secret);
if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    exit('Invalid signature');
}

$body = json_decode($payload, true);
$event = $body['event'];
$data = $body['data'];

error_log("Received event: $event");

switch ($event) {
    case 'score.changed':
        if ($data['direction'] === 'down') {
            // Alert team
            sendSlackAlert("Compliance score dropped: {$data['previous']} → {$data['current']}");
        }
        break;
    case 'regulation.new':
        saveRegulation($data);
        break;
}

header('Content-Type: application/json');
echo json_encode(['received' => true]);
go
// Go + net/http
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "io"
    "log"
    "net/http"
    "os"
)

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)

    secret := []byte(os.Getenv("COMPLIANCEIQ_WEBHOOK_SECRET"))
    mac := hmac.New(sha256.New, secret)
    mac.Write(body)
    expected := hex.EncodeToString(mac.Sum(nil))
    signature := r.Header.Get("X-Aegis Firma-Signature")

    if !hmac.Equal([]byte(expected), []byte(signature)) {
        http.Error(w, "Invalid signature", http.StatusUnauthorized)
        return
    }

    var payload struct {
        Event string          `json:"event"`
        Data  json.RawMessage `json:"data"`
    }
    json.Unmarshal(body, &payload)

    log.Printf("Received event: %s", payload.Event)

    switch payload.Event {
    case "score.changed":
        // Handle score change
        var data struct {
            Previous  int    `json:"previous"`
            Current   int    `json:"current"`
            Direction string `json:"direction"`
        }
        json.Unmarshal(payload.Data, &data)
        if data.Direction == "down" {
            sendAlert(data.Previous, data.Current)
        }
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]bool{"received": true})
}

Retry policy

Timeout

10 seconds per request. Return 2xx quickly — do heavy processing async.

Retries

3 attempts on non-2xx or timeout: immediately, +30s, +5min.

Idempotency

Use the event timestamp + org_id to deduplicate retried events.

Testing your webhook

From the Integrations page, click the Test button on any webhook — this fires a test.ping event to your endpoint.

Use webhook.site to inspect raw payloads during development.

Next steps