Who this is for: DevOps engineers configuring SIEM ingestion, GRC platform architects connecting compliance feeds, and security teams building alert pipelines. Requires Enclave tier ($9,500/mo) or above.

1. Overview

SWT3 Regulatory Webhooks push real-time compliance events to your infrastructure. When a verdict is recorded, a control drifts, or hardware attestation goes stale, your webhook endpoint receives an HMAC-signed JSON payload within seconds.

Use webhooks to:

Tier requirement: Webhooks are available on Enclave ($9,500/mo) and Sovereign tiers only. OPEN and PRO tiers receive a 403 "Enclave tier required" response. Upgrade information.

2. Quick Start (5 Steps)

1

Create a Subscription

curl -X POST https://sovereign.tenova.io/api/v1/webhooks \
  -H "Cookie: axiom_session=..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-server.com/swt3-webhook",
    "event_types": ["verdict.failed", "drift.detected"],
    "description": "Security alerts"
  }'

Response includes a secret field (whsec_...). Copy it immediately -- it is shown once.

2

Send a Test Ping

curl -X POST https://sovereign.tenova.io/api/v1/webhooks/{id}/test \
  -H "Cookie: axiom_session=..."

Your endpoint should receive a ping event. If it does not, check the delivery history (Step 5).

3

Deploy Your Receiver

Your webhook receiver must:

4

Validate the HMAC Signature

Every delivery includes an X-SWT3-Signature header. Verify it before processing. See Section 5 for code examples.

5

Go Live

Add the remaining event types you need. Monitor delivery health at GET /api/v1/webhooks/{id}/deliveries.

3. Event Types

Event TypeFires WhenTypical Use
verdict.issuedA PASS verdict is recorded in the ledgerEvidence stream, compliance dashboards
verdict.failedA FAIL verdict is recordedAlerts, incident response triggers
drift.detectedA control changes from PASS to FAIL between scansCA-7 continuous monitoring, SIEM correlation
attestation.lapsedA manual attestation expires without renewalCompliance officer alerts
score.thresholdSovereign Score drops below configured thresholdExecutive dashboards, SLA monitoring
hw.attestation.staleHardware attestation exceeds staleness windowInfrastructure monitoring
hw.drift.detectedHardware configuration changes detected on an agentSupply chain integrity, SBOM drift
pingTest event sent via the test endpointConnectivity verification

4. Payload Format

Every webhook delivery is a POST request with this JSON structure:

{
  "event_id": "evt_a1b2c3d4e5f6",
  "event_type": "verdict.failed",
  "timestamp": "2026-08-13T12:00:00.000Z",
  "tenant_id": "YOUR_TENANT",
  "data": {
    "procedure_id": "AI-GRD.1",
    "verdict": "FAIL",
    "swt3_anchor": "SWT3-E-VULTR-AI-AI-GRD.1-FAIL-1786624093-abc123def456",
    "factor_a": 2,
    "factor_b": 0,
    "factor_c": 0,
    "clearing_level": 1,
    "ai_model_id": "gpt-4o",
    "verification_url": "/api/v1/attest/verify?token=SWT3-..."
  }
}
FieldTypeDescription
event_idstringUnique event identifier for idempotency
event_typestringOne of the 8 event types above
timestampstringISO 8601 UTC timestamp
tenant_idstringYour tenant identifier
dataobjectEvent-specific payload (varies by event type)
Idempotency: Use event_id to deduplicate. In rare cases (network retry), the same event may be delivered twice. Your receiver should be idempotent -- processing the same event_id twice should have no side effects.

5. HMAC Signature Verification

Every delivery includes an X-SWT3-Signature header containing the HMAC-SHA256 hex digest of the raw request body, signed with your webhook secret.

Verification Steps

  1. Read the raw request body as bytes (do NOT parse JSON first).
  2. Compute HMAC-SHA256 using your webhook secret as the key and the raw body as the message.
  3. Compare the computed hex digest with the X-SWT3-Signature header using constant-time comparison.
  4. If they match, process the event. If not, return 401 and discard the request.
Common mistake: Parsing the JSON body first, then re-serializing it for HMAC verification. JSON serialization may reorder keys or change whitespace, producing a different digest. Always verify against the raw bytes.
PYTHON
TYPESCRIPT
GO
RUBY
import hmac, hashlib, json
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = "whsec_your_secret_here"

@app.route("/swt3-webhook", methods=["POST"])
def handle_webhook():
    signature = request.headers.get("X-SWT3-Signature", "")
    body = request.get_data()  # raw bytes, NOT request.json

    expected = hmac.new(
        WEBHOOK_SECRET.encode(), body, hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(expected, signature):
        abort(401, "Invalid signature")

    event = json.loads(body)
    print(f"Received {event['event_type']}: {event['data']['procedure_id']}")
    return "OK", 200
import { createHmac, timingSafeEqual } from "crypto";
import express from "express";

const app = express();
const WEBHOOK_SECRET = "whsec_your_secret_here";

app.post("/swt3-webhook", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.headers["x-swt3-signature"] as string;
  const body = req.body; // raw Buffer from express.raw()

  const expected = createHmac("sha256", WEBHOOK_SECRET)
    .update(body)
    .digest("hex");

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

  const event = JSON.parse(body.toString());
  console.log(`Received ${event.event_type}: ${event.data.procedure_id}`);
  res.status(200).send("OK");
});
package main

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

const webhookSecret = "whsec_your_secret_here"

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)
    signature := r.Header.Get("X-SWT3-Signature")

    mac := hmac.New(sha256.New, []byte(webhookSecret))
    mac.Write(body)
    expected := hex.EncodeToString(mac.Sum(nil))

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

    w.WriteHeader(http.StatusOK)
    w.Write([]byte("OK"))
}
require "sinatra"
require "openssl"
require "json"

WEBHOOK_SECRET = "whsec_your_secret_here"

post "/swt3-webhook" do
  body = request.body.read
  signature = request.env["HTTP_X_SWT3_SIGNATURE"]

  expected = OpenSSL::HMAC.hexdigest("SHA256", WEBHOOK_SECRET, body)

  unless Rack::Utils.secure_compare(expected, signature)
    halt 401, "Invalid signature"
  end

  event = JSON.parse(body)
  puts "Received #{event['event_type']}: #{event['data']['procedure_id']}"
  status 200
  "OK"
end

6. Delivery and Retries

AttemptDelayTotal Wait
1st (initial)Immediate0s
2nd (retry 1)10 seconds10s
3rd (retry 2)60 seconds70s

After 3 failed attempts, the delivery is marked as failed. The subscription remains active for future events.

Delivery States

StateMeaning
pendingQueued, not yet attempted
successDelivered, received HTTP 2xx
retryingFailed, retry scheduled
failedAll retry attempts exhausted

What Counts as Success

Any HTTP 200-299 response within 10 seconds. Your endpoint must respond within this window or the delivery times out and retries.

Response Body Capture

The first 1,024 bytes of your endpoint's response body are captured and stored in the delivery record. This helps debug issues when your endpoint returns an error message.

7. Debugging Checklist

If your webhook is not firing, work through this checklist in order:

  1. Tier check: Are you on Enclave or Sovereign? GET /api/v1/health does not require auth, but webhooks do require Enclave+.
  2. Subscription active? GET /api/v1/webhooks -- check is_active: true.
  3. Event types match? If you subscribed to verdict.failed but all inferences are PASS, no webhook fires. Add verdict.issued for PASS events.
  4. URL reachable? Your endpoint must be publicly accessible via HTTPS. Test: curl -I https://your-server.com/swt3-webhook.
  5. Send a test ping: POST /api/v1/webhooks/{id}/test. This sends a ping event immediately.
  6. Check delivery history: GET /api/v1/webhooks/{id}/deliveries. Look at state, status_code, and response_body.
  7. Check your server logs: Is the request arriving? Is HMAC verification failing? Is your endpoint returning 500?
  8. Firewall/WAF: Some WAFs block POST requests from unfamiliar user agents. Allowlist the SWT3 webhook user agent or IP range.
Quick diagnosis: GET /api/v1/webhooks/{id}/deliveries?limit=5 shows the last 5 delivery attempts with HTTP status codes and response bodies. This tells you whether the problem is on your side (500) or on the network (timeout).

8. Managing Subscriptions

POST /api/v1/webhooks Session (Admin) Enclave+
curl -X POST https://sovereign.tenova.io/api/v1/webhooks \
  -H "Cookie: axiom_session=..." \
  -H "Content-Type: application/json" \
  -d '{"url":"https://hooks.slack.com/services/T.../B.../xxx","event_types":["verdict.failed"],"description":"Slack FAIL alerts"}'
GET /api/v1/webhooks Session (Admin)

List all subscriptions with last delivery status. Secrets are masked.

PUT /api/v1/webhooks/:id Session (Admin)

Update URL, event types, or active status. Cannot change the secret (delete and recreate instead).

DELETE /api/v1/webhooks/:id Session (Admin)

Delete a subscription. Delivery history is retained for audit purposes.

POST /api/v1/webhooks/:id/test Session (Admin)

Send a test ping event. Returns the delivery result including HTTP status and response body.

GET /api/v1/webhooks/:id/deliveries Session (Admin)

View delivery history. Includes state, HTTP status code, response body (first 1,024 bytes), and timestamp.

9. Integration Patterns

Slack: FAIL Alerts

Create a Slack Incoming Webhook and point the SWT3 webhook at it. Subscribe to verdict.failed and drift.detected. Slack will display the raw JSON payload -- for formatted messages, use a middleware (AWS Lambda, Cloudflare Worker) that transforms the payload into Slack Block Kit format.

Datadog: Compliance Events

Forward webhook payloads to the Datadog HTTP API (POST /api/v2/logs). Map event_type to Datadog tags. Use verdict.issued and verdict.failed for full evidence streams. Build dashboards on procedure_id facets.

PagerDuty: Incident Triggers

Use the PagerDuty Events API v2. Map verdict.failed to trigger and drift.detected to change. Include the swt3_anchor in the incident body for traceability.

ServiceNow: GRC Integration

Create a ServiceNow Scripted REST API endpoint. Map SWT3 events to GRC policy exceptions or control test results. The procedure_id maps to your ServiceNow control catalog. See the GRC Platform Integration Guide for detailed field mapping.

10. Limits and Constraints

ConstraintValue
Max subscriptions per tenant5
URL protocolHTTPS required (HTTP allowed for localhost only)
Response timeout10 seconds
Retry attempts3 (initial + 2 retries)
Retry backoff0s, 10s, 60s
Response body capturedFirst 1,024 bytes
Minimum tierEnclave ($9,500/mo)

Secret Rotation

Webhook secrets cannot be rotated in place. To rotate: delete the subscription and create a new one. Your endpoint will receive events with the new secret. Plan for a brief gap during the switchover.

For the complete endpoint reference, see the API Reference -- Webhooks. For common issues, see the Troubleshooting FAQ -- Webhooks.