Tenant webhooks
Subscribe to tenant webhooks, verify HMAC signatures with rank.verify_signature, and list deliveries.
What you’ll build
A tenant webhook: one HTTPS subscription that fires for every pentest of its owner (you,
or a team). You register the URL and events, store the signing secret (returned once), verify
each delivery with rank.verify_signature, and list delivery history.
Prefer tenant webhooks over legacy per-pentest hooks (/pentests/{id}/webhooks). Integrations
(Jira / GitHub / Slack) are a different channel — see Push findings to Jira, GitHub or Slack.
The full event catalog (finding, pentest, control) is on Webhooks.
Prerequisites
- A Rank account and an API token (see Authentication).
- An HTTPS endpoint Rank can reach (SSRF-checked: no private IPs, localhost, or cloud metadata).
pip install rank-sdk
export RANK_API_KEY=rk_...
export RANK_WEBHOOK_URL=https://hooks.example.com/rank
Steps
-
Subscribe. If you omit
secret, Rank generates one and returns it once.import rank client = rank.Rank() created = client.webhooks.create( url="https://hooks.example.com/rank", events=[ "vulnerability.validated", "vulnerability.retested", "pentest.completed", "control.kill_requested", "control.approval_requested", ], description="SOC inbox", ) secret = created.secret webhook_id = created.webhook.id print(secret)Subscribe to
vulnerability.createdif you want every finding as soon as an agent files it; subscribe only tovulnerability.validatedif you do not want developers paged until a deterministic validator has reproduced the issue. -
Verify each delivery. The signature is HMAC-SHA256 of
{timestamp}.{raw_body}(not the body alone), headerX-Rank-Signature: v1=…, 5-minute window. Sign the raw bytes; re-serializing JSON will not match.rank.verify_signature(raw_body, headers, secret)It raises
rank.SignatureVerificationErrorif the timestamp is missing, stale, or the HMAC does not match. Respond2xxin under 10 seconds. -
Send a signed
ping, then list deliveries (pending,delivered,dead). History is retained 30 days.ping = client.webhooks.test(webhook_id) print(ping.message, ping.delivery.status if ping.delivery else None) deliveries = client.webhooks.list_deliveries(webhook_id) for d in deliveries.items: print(d.id, d.event, d.status, d.http_status)
-
Subscribe.
curl https://api.aleex-rank.ai/api/v2/webhooks \ -H "X-API-Key: $RANK_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://hooks.example.com/rank", "events": [ "vulnerability.validated", "vulnerability.retested", "pentest.completed", "control.kill_requested", "control.approval_requested" ], "description": "SOC inbox" }'Store
data.secretnow; listings never return it. -
Verify HMAC of
{timestamp}.{raw_body}againstX-Rank-Signature(v1=) and reject timestamps older than about five minutes. Equivalent helper:rank.verify_signature. -
Ping and list deliveries.
POST /api/v2/webhooks/18/test GET /api/v2/webhooks/18/deliveries GET /api/v2/webhooks/18/deliveries/918273 POST /api/v2/webhooks/18/deliveries/918273/redeliver X-API-Key: rk_...
Event names are append-only. Finding events carry vulnerability_id; pentest and control events
carry pentest_id. See the tables on Webhooks.
Run it
Save the following as tenant_webhooks.py, set RANK_API_KEY and RANK_WEBHOOK_URL, then run
python tenant_webhooks.py.
"""Subscribe to a tenant webhook, demonstrate signature verification, list deliveries.
What this script does:
1. Registers a tenant webhook (secret returned once).
2. Builds a genuine HMAC and verifies it with rank.verify_signature.
3. Sends a signed ping and lists recent deliveries.
Run:
pip install rank-sdk
export RANK_API_KEY=rk_...
export RANK_WEBHOOK_URL=https://hooks.example.com/rank
python tenant_webhooks.py
"""
from __future__ import annotations
import hashlib
import hmac
import os
import time
import rank
WEBHOOK_URL = os.environ.get("RANK_WEBHOOK_URL", "")
def main() -> None:
if not WEBHOOK_URL:
raise SystemExit("ERROR: set RANK_WEBHOOK_URL to an HTTPS endpoint.")
with rank.Rank() as client:
created = client.webhooks.create(
url=WEBHOOK_URL,
events=[
"vulnerability.validated",
"vulnerability.retested",
"pentest.completed",
"control.kill_requested",
"control.approval_requested",
],
description="SOC inbox",
)
hook = created.webhook
secret = created.secret
print(f"Webhook id={hook.id if hook else '?'} secret={secret}")
if secret:
raw = b'{"id":"demo","type":"ping","version":2}'
timestamp = str(int(time.time()))
digest = hmac.new(
secret.encode(),
timestamp.encode() + b"." + raw,
hashlib.sha256,
).hexdigest()
headers = {
"X-Rank-Timestamp": timestamp,
"X-Rank-Signature": "v1=" + digest,
}
rank.verify_signature(raw, headers, secret)
print("verify_signature: ok")
if hook is not None:
ping = client.webhooks.test(hook.id)
delivery = ping.delivery
print(
f"Ping: {ping.message} "
f"status={delivery.status if delivery else None}"
)
deliveries = client.webhooks.list_deliveries(hook.id)
print(f"Deliveries on this page: {len(deliveries.items)}")
for d in deliveries.items[:5]:
print(f" [{d.id}] {d.event} {d.status} http={d.http_status}")
if __name__ == "__main__":
try:
main()
except rank.AuthenticationError:
print("ERROR: invalid or missing API key. Set RANK_API_KEY.")
except rank.SignatureVerificationError as exc:
print(f"Signature error: {exc}")
except rank.APIError as exc:
print(f"API error ({exc.status_code}): {exc.message}")