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"
}
}
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)
}
Node.js Example
const crypto = require("crypto");
const express = require("express");
const app = express();
const WEBHOOK_SECRET = "whsec_your_secret_here";
app.post("/webhooks/certix", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.headers["x-webhook-signature"] || "";
if (!signature.startsWith("sha256=")) {
return res.status(401).send("Invalid signature");
}
const received = signature.slice(7);
const expected = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(req.body)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected))) {
return res.status(401).send("Invalid signature");
}
const payload = JSON.parse(req.body);
console.log(`Event: ${payload.event}, Scan: ${payload.data.scanId}`);
res.status(200).send("OK");
});
Always use constant-time comparison functions (hmac.compare_digest in Python, hmac.Equal in Go, crypto.timingSafeEqual in Node.js) to prevent timing attacks.
Manage Webhooks
List Webhooks
GET /api/v1/webhooks
Required scope: webhooks:read
Get a Webhook
GET /api/v1/webhooks/:webhookId
Update a Webhook
PATCH /api/v1/webhooks/:webhookId
Required scope: webhooks:update
curl -X PATCH https://api.cert-ix.com/scan-api/api/v1/webhooks/$WEBHOOK_ID \
-H "X-API-Key: $CERTIX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"events": ["scan.completed", "scan.failed", "scan.started"]
}'
Delete a Webhook
DELETE /api/v1/webhooks/:webhookId
Required scope: webhooks:delete
Test a Webhook
Send a test event to verify your webhook endpoint is reachable and correctly configured.
POST /api/v1/webhooks/:webhookId/test
Required scope: webhooks:create
{
"success": true,
"data": {
"delivered": true,
"statusCode": 200,
"responseTime": 145,
"event": "webhook.test"
}
}
Delivery Logs
View delivery history for a webhook, including success/failure status, response codes, and processing times.
GET /api/v1/webhooks/:webhookId/deliveries
Required scope: webhooks:read
{
"success": true,
"data": {
"deliveries": [
{
"id": "del-uuid-1",
"event": "scan.completed",
"statusCode": 200,
"success": true,
"responseTime": 145,
"attempt": 1,
"deliveredAt": "2026-03-06T10:02:15Z"
}
]
}
}
Retry Logic
Failed deliveries are automatically retried with exponential backoff:
| Attempt | Delay | Total Elapsed |
|---|---|---|
| 1 | Immediate | 0 |
| 2 | 60 seconds | 1 minute |
| 3 | 120 seconds | 3 minutes |
| 4 (final) | 240 seconds | 7 minutes |
A delivery is considered failed if:
- Your endpoint returns a non-2xx HTTP status code
- The connection times out (30-second timeout)
- DNS resolution fails
- TLS handshake fails
Health Monitoring
The platform tracks webhook health based on delivery success:
| Field | Description |
|---|---|
isHealthy | true if recent deliveries succeed |
consecutiveFailures | Count of consecutive failed deliveries |
lastTriggeredAt | Timestamp of last delivery attempt |
lastStatusCode | HTTP status code of last delivery |
Auto-Disable
If a webhook accumulates 10 consecutive failures, it's automatically disabled (isActive: false). To re-enable:
- Fix the issue with your endpoint
- Re-activate via
PATCHwith{"isActive": true} - Send a test event to verify
Best Practices
- ✅ Use HTTPS only — Webhook URLs must use
https:// - ✅ Verify signatures on every request
- ✅ Return 200 quickly — Process events asynchronously to avoid timeouts
- ✅ Implement idempotency — The same event may be delivered more than once during retries
- ✅ Validate event type — Only process expected events
Next Steps: