Skip to content
Docs/Architecture/Webhooks & Events
Edit on GitHub

Webhooks & Events

Platforms in the opensecstack ecosystem communicate through signed, replay-protected webhooks rather than ad-hoc HTTP calls. Every webhook uses the same HMAC-SHA256 signing scheme, a ±5-minute replay window, and per-source shared secrets. This page is the cross-platform reference — consult the individual platform docs for endpoint-specific details.

Overview

Two platforms act as primary webhook receivers in the ecosystem:

  • IRFlow — receives events from APIGuard, CITADEL, and ThreatFlow to create or enrich incidents automatically.
  • ThreatFlow — receives events from APIGuard, CITADEL, and generic external sources; pushes outbound events to IRFlow, NIS2Compass, and OpenCSIRT.

All webhook endpoints share one signing scheme that differs only in the header names and per-source secret used. No JWT is required — authentication is entirely via HMAC.

PlatformEndpointSourceTypical trigger
IRFlowPOST /api/v1/webhooks/apiguardAPIGuardFinding crosses severity threshold or scan completes
IRFlowPOST /api/v1/webhooks/citadelCITADELHARD_STOP decisions, WORM anchor events
IRFlowPOST /api/v1/webhooks/threatflowThreatFlowIOC bundle published, correlation match
ThreatFlowPOST /api/v1/webhooks/apiguardAPIGuardScan completed, critical finding
ThreatFlowPOST /api/v1/webhooks/citadelCITADELMARSHAL decision, AUGUR advisory, VIGIL alert
ThreatFlowPOST /api/v1/webhooks/genericThird-partyCustom IOC report from external scanner

Wire format & signing

Every signed webhook request carries two mandatory headers. The canonical signing input is the Unix timestamp, a literal dot, and the raw request body — exactly as received on the wire, with no re-serialisation or whitespace normalisation.

Header (IRFlow)Header (ThreatFlow)Description
X-Irflow-TimestampX-ThreatFlow-TimestampUnix seconds at signing time (UTC)
X-Irflow-SignatureX-ThreatFlow-Signaturesha256=<hex> — HMAC-SHA256 over the canonical input
X-Irflow-Event-Id (optional)X-ThreatFlow-DeliveryUUID for sender-side deduplication and idempotency
—X-ThreatFlow-EventEvent type string (e.g. apiguard.scan.completed)

The canonical signing formula used by both platforms is:

Bash
signed_payload = timestamp + "." + raw_body
signature      = hex(HMAC-SHA256(per_source_secret, signed_payload))
header_value   = "sha256=" + signature
Constant-time comparison: Always use a constant-time byte-comparison function (e.g. hmac.Equal in Go, hmac.compare_digest in Python, timingSafeEqual in Node.js) when verifying the signature — standard string equality leaks timing information.

Replay protection

Both IRFlow and ThreatFlow reject requests whose timestamp falls outside a ±5-minute window of the server's current time. IRFlow exposes the tolerance via the environment variable IRFLOW_WEBHOOK_CLOCK_SKEW_TOLERANCE (default 5m); ThreatFlow hard-codes maxTimestampSkew = 300 seconds.

The ±5-minute window balances capture-replay risk against real-world NTP drift between peers. A tighter window (e.g. 30 s) is possible but requires well-synchronised clocks across all sender platforms. Requests outside the window return 401 with a structured JSON error body.

ThreatFlow also issues a unique X-ThreatFlow-Delivery header (UUID v4) per delivery. Receivers should use this ID to deduplicate retried deliveries. IRFlow logs the optional X-Irflow-Event-Id for the same purpose; v1.1 will persist and reject duplicate IDs.

Per-source secrets

Each source platform has its own shared secret. Never reuse a secret across sources. Secrets are configured on the receiver side. If a per-source secret is not set, the corresponding endpoint returns 503 until configured — this is fail-closed by design.

ReceiverEnvironment variableCovers
IRFlowIRFLOW_WEBHOOK_APIGUARD_SECRETAPIGuard endpoint verifier
IRFlowIRFLOW_WEBHOOK_CITADEL_SECRETCITADEL endpoint verifier
IRFlowIRFLOW_WEBHOOK_THREATFLOW_SECRETThreatFlow endpoint verifier
ThreatFlowTHREATFLOW_APIGUARD_WEBHOOK_SECRETInbound APIGuard events
ThreatFlowTHREATFLOW_CITADEL_KEY_SECRETInbound CITADEL events
ThreatFlowTHREATFLOW_IRFLOW_WEBHOOK_SECRETOutbound deliveries to IRFlow
Legacy variable: IRFLOW_WEBHOOK_SECRET exists as a legacy fallback and will be removed in IRFlow v1.1. Do not use it for new deployments.

Signed-webhook example

The following shows a complete IRFlow inbound webhook from ThreatFlow — a correlation match payload with the signing headers already populated. The same shape applies to APIGuard and CITADEL payloads with their respective event types.

signed-webhook-request.sh
Bash
"cm"># Signed webhook from ThreatFlow -> IRFlow
"cm"># Headers
X-Irflow-Timestamp: 1745056462
X-Irflow-Signature: sha256=3a7f1c84e2d09b56f8ac4210edb3f70a92651cc8fa2e47d13b09e5a7c682b41f
X-Irflow-Event-Id: tf-bundle-0000042
Content-Type: application/json

"cm"># Body
{
  "event_id":   "tf-bundle-0000042",
  "event_type": "threatflow.bundle.published",
  "bundle_id":  "tf_bundle_789",
  "incident_id": "inc_123",
  "iocs": [
    {"type": "ipv4",   "value": "203.0.113.5",       "confidence": 0.92},
    {"type": "sha256", "value": "abc123...",          "confidence": 0.81},
    {"type": "domain", "value": "malicious.example", "confidence": 0.77}
  ],
  "occurred_at": "2026-04-19T10:14:22Z"
}

"cm"># Signature computation (pseudo-code):
"cm"># signed_payload = "1745056462" + "." + raw_body_bytes
"cm"># signature      = hex(HMAC-SHA256(IRFLOW_WEBHOOK_THREATFLOW_SECRET, signed_payload))
"cm"># header         = "sha256=" + signature

The SDK ships pre-built sender clients that handle signing automatically — prefer those over rolling your own implementation.

Event & contract types

The SDK defines a typed Event envelope that all platforms share. The common fields are event_id, event_type, source, ts_utc, and an optional project_id, with a payload map carrying event-specific data.

Event typeSourceConsumer(s)IRFlow action
apiguard.scan.completedAPIGuardIRFlow, ThreatFlowLog; attach to nearest open incident for project
apiguard.finding.criticalAPIGuardIRFlow, ThreatFlowAuto-create P1 incident
apiguard.finding.* (high)APIGuardIRFlowAuto-create P2 incident
citadel.marshal.hard_stopCITADELIRFlow, ThreatFlowAlways creates P1 incident; triggers project freeze playbook
citadel.marshal.decisionCITADELThreatFlowMARSHAL governance decision delivered
citadel.augur.advisoryCITADELThreatFlowMay block ingestion pipeline
citadel.vigil.alertCITADELThreatFlowVIGIL monitoring alert
citadel.hard_stopCITADELSDK / customWORM-logged; irflow_incident_id in payload
citadel.vigil_redCITADELSDK / customVIGIL transitioned to RED state
threatflow.bundle.publishedThreatFlowIRFlowAttach IOCs to open incident if incident_id present
threatflow.correlation.matchThreatFlowIRFlowIOC matched to active finding or incident
threatflow.ioc.ingestedThreatFlowIRFlowNew IOC persisted to store
threatflow.ioc.revokedThreatFlowIRFlowIOC expired or manually revoked
threatflow.bundle.exportedThreatFlowNIS2Compass, OpenCSIRTSTIX bundle exported for downstream compliance use
nis2compass.control.updatedNIS2CompassSDK / customControl status changed in an assessment
nis2compass.assessment.completedNIS2CompassSDK / customAll controls reached terminal status
SDK event router: The SDK ships a typed event router (webhook.NewRouter in Go) that dispatches by event_type and handles signature verification before routing. Custom integrations should use the router rather than parsing the envelope manually.

Error responses

Both platforms return structured JSON errors: {"error": "human-readable description"}.

StatusMeaning
200 / 202Accepted — processed synchronously or queued
400Payload failed JSON decode or schema validation
401Signature missing, timestamp outside skew, or HMAC mismatch
413Body exceeds IRFLOW_WEBHOOK_MAX_BODY_SIZE (default 1 MiB) or ThreatFlow body limit
429Rate limit exceeded (ThreatFlow: 100 inbound requests/min per source IP)
503Per-source secret not configured — endpoint disabled (fail-closed)

Retry & dead-letter (ThreatFlow)

When an outbound delivery from ThreatFlow fails, it retries with exponential backoff. After all attempts are exhausted, the delivery moves to a dead-letter queue for manual inspection and replay.

AttemptDelayTotal elapsed
1 (initial)0 s0 s
2 (first retry)30 s30 s
3 (second retry)60 s90 s
4 (third retry)120 s210 s

401, 403, and 404 responses are non-retryable — retrying will not change the outcome. Network errors, timeouts, and 5xx responses are retried. Dead-letter items are retained for 30 days (configurable) and can be replayed via the ThreatFlow admin API.

Event delivery guarantees: Webhook delivery is best-effort. Always poll the source platform's REST API as a fallback for critical alerting paths — do not rely solely on webhooks.

SIN — Security Intelligence Network — Open-source cybersecurity & compliance for the EU Digital Decade.

Apache-2.0 & AGPL-3.0 · GitHub · Contribute