Webhooks
Webhooks provide real-time HTTP push notifications when scan events occur, eliminating the need for polling. When a scan completes, fails, or triggers other events, the Cert-IX platform sends an HTTP POST request to your registered URL with the event payload.
Why Use Webhooks?​
- Instant notifications — No polling delay
- Reduced API calls — No quota consumed for status checks
- Event-driven architecture — Automatically trigger CI/CD gates, notifications, ticket creation
- Reliable delivery — Automatic retries with exponential backoff
Register a Webhook​
Endpoint​
POST /api/v1/webhooks
Required scope: webhooks:create
Request​
curl -X POST https://api.cert-ix.com/scan-api/api/v1/webhooks \
-H "X-API-Key: $CERTIX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "CI/CD Scan Notifications",
"url": "https://your-server.example.com/webhooks/certix",
"events": ["scan.completed", "scan.failed"]
}'
Request Parameters​
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Human-readable webhook name (max 255 chars) |
url | string | Yes | HTTPS endpoint to receive events (max 2048 chars) |
events | string[] | No | Event types to subscribe to (default: scan.completed, scan.failed) |
Response (201 Created)​
{
"success": true,
"data": {
"id": "wh-a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "CI/CD Scan Notifications",
"url": "https://your-server.example.com/webhooks/certix",
"secret": "whsec_a8b2f1d92ffb23344a943df2b6a001fb1028d002e3f4a5b6c7d8",
"events": ["scan.completed", "scan.failed"],
"isActive": true,
"isHealthy": true,
"createdAt": "2026-03-06T10:00:00Z"
}
}
Store the Secret
The secret field is returned only at creation. Store it securely — you'll need it to verify webhook signatures.
Webhook Events​
| Event | Trigger | Description |
|---|---|---|
scan.completed | Scan finishes successfully | Results are ready to retrieve |
scan.failed | Scan encounters a fatal error | Error details included in payload |
scan.started | Scan begins execution | Transition from queued to running |
scan.cancelled | Scan is cancelled | Partial results may be available |
If events is omitted, the webhook defaults to ["scan.completed", "scan.failed"].
Webhook Payload Format​
When an event occurs, the platform sends an HTTP POST to your webhook URL.
Headers​
Content-Type: application/json
X-Webhook-Signature: sha256=a1b2c3d4e5f6...
X-Webhook-Event: scan.completed
X-Webhook-Delivery: del-uuid-here
X-Webhook-Timestamp: 2026-03-06T10:02:15Z
Body​
{
"event": "scan.completed",
"timestamp": "2026-03-06T10:02:15Z",
"tenantId": "7b5b0610-2947-412f-a869-4683da321fcf",
"data": {
"scanId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"scanType": "nmap",
"name": "Weekly network audit",
"target": "example.com",
"status": "completed",
"progress": 100,
"resultCount": 12,
"durationMs": 135000,
"createdAt": "2026-03-06T10:00:00Z",
"completedAt": "2026-03-06T10:02:15Z"
}
}
Signature Verification​
Every webhook delivery is signed with your webhook secret using HMAC-SHA256. Always verify signatures to ensure the request originates from Cert-IX and hasn't been tampered with.
Verification Algorithm​
- Extract the
X-Webhook-Signatureheader - Get the raw request body (before any JSON parsing)
- Compute
HMAC-SHA256(secret, body)and hex-encode - Compare against the header signature (use constant-time comparison)
Python Example​
import hmac
import hashlib
def verify_webhook(request_body: bytes, signature_header: str, secret: str) -> bool:
"""Verify Cert-IX webhook signature."""
if not signature_header.startswith("sha256="):
return False
received = signature_header[7:]
expected = hmac.new(
secret.encode("utf-8"),
request_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(received, expected)
# Flask example
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = "whsec_your_secret_here"
@app.route("/webhooks/certix", methods=["POST"])
def handle_webhook():
signature = request.headers.get("X-Webhook-Signature", "")
if not verify_webhook(request.data, signature, WEBHOOK_SECRET):
abort(401, "Invalid signature")
payload = request.get_json()
event = payload["event"]
if event == "scan.completed":
scan_id = payload["data"]["scanId"]
# Process completed scan...
return "", 200
Go Example​
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"strings"
)
func verifyWebhook(body []byte, signatureHeader, secret string) bool {
if !strings.HasPrefix(signatureHeader, "sha256=") {
return false
}
received := signatureHeader[7:]
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(received), []byte(expected))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
signature := r.Header.Get("X-Webhook-Signature")
if !verifyWebhook(body, signature, "whsec_your_secret_here") {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Process event...
w.WriteHeader(http.StatusOK)
}