Webhooks
Register HTTPS webhooks on a pentest to receive vulnerability lifecycle events, verify their HMAC signature, and consume the payload.
All paths are relative to https://api.aleex-rank.ai/api/v2 and authenticate with X-API-Key: rk_... (see REST API). Webhooks let you react to vulnerability changes in real time — notify Slack, kick off a pipeline, or sync findings into your own tracker — instead of polling.
Webhooks are registered per pentest and are gated to Enterprise teams (see Teams & tiers).
Register a webhook
POST /pentests/{id}/webhooks
Content-Type: application/json
| Field | Type | Required | Notes |
|---|---|---|---|
url | string | Yes | Must be a valid HTTPS URL |
events | string[] | Yes | At least one event from the list below |
secret | string | No | Min 16 characters; enables signature verification |
{
"url": "https://ci.example.com/hooks/rank",
"secret": "a-long-random-secret-value",
"events": ["vulnerability.resolved", "vulnerability.status_changed"]
}
Response:
{
"success": true,
"data": {
"message": "Webhook registered successfully",
"webhook_id": 7,
"url": "https://ci.example.com/hooks/rank",
"events": ["vulnerability.resolved", "vulnerability.status_changed"]
}
}
List and delete
GET /pentests/{id}/webhooks
DELETE /pentests/{id}/webhooks/{hookId}
{
"success": true,
"data": {
"webhooks": [
{"id": 7, "url": "https://ci.example.com/hooks/rank", "events": ["vulnerability.resolved"]}
],
"total": 1
}
}
Secrets are never returned in listings.
Events
| Event | Fires when |
|---|---|
vulnerability.created | A new finding is created (e.g. agents surface it during a run) |
vulnerability.status_changed | A finding changes state |
vulnerability.assigned | A finding is assigned or unassigned |
vulnerability.resolved | A finding is resolved |
vulnerability.reopened | A finding is reopened |
vulnerability.commented | A comment is added to a finding |
Payload
Each delivery is an HTTP POST with a JSON body wrapping the event name and a payload object:
{
"event": "vulnerability.resolved",
"payload": {
"vulnerability_id": 42,
"pentest_id": 10,
"triggered_by": 1,
"timestamp": "2026-02-10T15:00:00+00:00",
"old_status": "in_progress",
"new_status": "resolved",
"resolution_type": "evidenced"
}
}
The exact payload fields vary by event (an assigned event carries the assignee, a commented event carries the comment), so treat payload as event-specific.
Delivery headers
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | Rank-Webhook/1.0 |
X-Webhook-Event | the event name, e.g. vulnerability.resolved |
X-Webhook-Signature | sha256=<hmac> — only when a secret is configured |
Delivery is fire-and-forget with a short timeout: respond 2xx quickly and do heavy work asynchronously. Endpoints must be reachable over HTTPS with a valid certificate.
Verify the signature
When you set a secret, every delivery includes an X-Webhook-Signature header containing the HMAC-SHA256 of the raw request body keyed by your secret. Compute the same digest over the exact bytes you received and compare in constant time. Reject the request if it doesn’t match.
import hmac
import hashlib
def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(f"sha256={expected}", signature_header or "")
import crypto from "node:crypto";
function verify(rawBody, signatureHeader, secret) {
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(`sha256=${expected}`);
const b = Buffer.from(signatureHeader || "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Example consumer
A minimal receiver that verifies the signature, then acts on the event:
from flask import Flask, request, abort
import hmac, hashlib, os
app = Flask(__name__)
SECRET = os.environ["RANK_WEBHOOK_SECRET"]
@app.post("/hooks/rank")
def rank_webhook():
raw = request.get_data() # raw bytes, before JSON parsing
sig = request.headers.get("X-Webhook-Signature", "")
expected = "sha256=" + hmac.new(SECRET.encode(), raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig):
abort(401)
body = request.get_json()
event = body["event"]
payload = body["payload"]
if event == "vulnerability.resolved":
notify_team(payload["vulnerability_id"], payload.get("resolution_type"))
return "", 200 # acknowledge fast
Register from CI
A common pattern is to register a webhook as part of a pipeline so a build is notified when criticals are resolved:
curl -X POST https://api.aleex-rank.ai/api/v2/pentests/10/webhooks \
-H "X-API-Key: rk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://ci.example.com/hooks/rank",
"secret": "a-long-random-secret-value",
"events": ["vulnerability.resolved", "vulnerability.reopened"]
}'
For pulling findings on demand rather than receiving pushes, pair webhooks with the Vulnerabilities API — for example fetch the summary or run a quality gate when an event arrives.