API reference

Webhooks

Get notified the instant a scan completes — no polling, no cron jobs. QorBOM POSTs a signed JSON envelope to your registered endpoint and retries on any non-2xx response.

Event types

EventWhen
cbom.scan.completedScan finished successfully — BOM is ready to download.
cbom.scan.failedScan errored (network failure, malformed repo, timeout). data.error describes the cause.

Payload shape

Every event has the same envelope. The data object varies by event type.

{
  "id":        "evt_8f2a1c0b9d3e",
  "type":      "cbom.scan.completed",
  "created":   "2026-05-31T11:24:18Z",
  "tenant_id": "tnt_abc123",
  "data": {
    "scan_id":   "cbom_a1b2c3d4e5f6g7h8",
    "repo_url":  "https://github.com/octocat/Hello-World",
    "status":    "completed",
    "score":     78,
    "components_total": 142,
    "findings_total":   6,
    "bom_url":   "https://api.qorbom.com/api/v1/cbom/scans/cbom_a1b2.../bom.json"
  },
  "methodology_version": "qortrace-cbom-method-v0.1"
}

Signature verification

Every request is signed with HMAC-SHA256 using your webhook signing secret (different from your API key — see your partner portal). Verify it before processing.

Header
X-QorBOM-Signature: t=<ts>,v1=<hex>

Comma-separated. t is the unix timestamp; v1 is HMAC-SHA256(secret, "t.body").

Replay protection

Reject events older than 5 minutes (now - t > 300). That guards against replay attacks if your signing secret ever leaks.

import hmac, hashlib, time
from fastapi import HTTPException, Request

SECRET = os.environ["QORBOM_WEBHOOK_SECRET"]
TOLERANCE = 300  # seconds

@app.post("/qorbom-webhook")
async def webhook(request: Request):
    body = await request.body()
    sig_header = request.headers.get("x-qorbom-signature", "")
    parts = dict(p.split("=", 1) for p in sig_header.split(","))
    t = int(parts.get("t", 0))
    v1 = parts.get("v1", "")
    if abs(time.time() - t) > TOLERANCE:
        raise HTTPException(400, "stale event")
    expected = hmac.new(
        SECRET.encode(),
        f"{t}.{body.decode()}".encode(),
        hashlib.sha256,
    ).hexdigest()
    if not hmac.compare_digest(expected, v1):
        raise HTTPException(400, "bad signature")
    # ... process event ...
    return {"ok": True}

Retries & delivery guarantees

We retry any non-2xx response with exponential backoff for up to 24 hours: 30s, 2m, 10m, 1h, 6h, 24h. Return 200 as soon as you've persisted the event; process asynchronously.

Idempotency. Deduplicate by id. We guarantee at-least-once delivery, not exactly-once — a flaky network in the response path can produce a duplicate even on a successful processor.

Register an endpoint

Webhook endpoints are configured per-partner in your portal at /qorbom/portal. Up to 5 endpoints per tenant; each gets its own signing secret.