Skip to main content
Version: Next 🚧

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​

FieldTypeRequiredDescription
namestringYesHuman-readable webhook name (max 255 chars)
urlstringYesHTTPS endpoint to receive events (max 2048 chars)
eventsstring[]NoEvent 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​

EventTriggerDescription
scan.completedScan finishes successfullyResults are ready to retrieve
scan.failedScan encounters a fatal errorError details included in payload
scan.startedScan begins executionTransition from queued to running
scan.cancelledScan is cancelledPartial 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​

  1. Extract the X-Webhook-Signature header
  2. Get the raw request body (before any JSON parsing)
  3. Compute HMAC-SHA256(secret, body) and hex-encode
  4. 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");
});
Security

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:

AttemptDelayTotal Elapsed
1Immediate0
260 seconds1 minute
3120 seconds3 minutes
4 (final)240 seconds7 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:

FieldDescription
isHealthytrue if recent deliveries succeed
consecutiveFailuresCount of consecutive failed deliveries
lastTriggeredAtTimestamp of last delivery attempt
lastStatusCodeHTTP status code of last delivery

Auto-Disable​

If a webhook accumulates 10 consecutive failures, it's automatically disabled (isActive: false). To re-enable:

  1. Fix the issue with your endpoint
  2. Re-activate via PATCH with {"isActive": true}
  3. 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: