Skip to content

Webhooks

The generic webhook channel POSTs a JSON document to a URL you control whenever a subscribed event fires. This page is the contract.

Request shape

AspectValue
MethodPOST
PathThe URL you configured for the channel
Content typeapplication/json; charset=utf-8
User agentHarborGuard-Webhook/1.0
Timeout10 seconds

Headers

HeaderDescription
Content-TypeAlways application/json
User-AgentHarborGuard-Webhook/1.0
X-HarborGuard-Signaturesha256=<hex> HMAC of the raw body using your channel signing secret. Present only when the channel has a secret configured.

Body schema

{
  "event": "critical_cve",
  "severity": "CRITICAL",
  "title": "Critical CVE detected: CVE-2025-12345",
  "message": "openssl 3.0.7 in nginx:1.25 is affected by CVE-2025-12345 (CVSS 9.8).",
  "url": "https://harborguard.co/dashboard/vulnerabilities/CVE-2025-12345",
  "timestamp": "2026-04-26T14:21:08.512Z"
}

Additional event-specific keys are included alongside the core fields. The event field is the event type; severity is one of CRITICAL, HIGH, MEDIUM, LOW, INFO.

Example payloads

scan_complete

{
  "event": "scan_complete",
  "severity": "INFO",
  "title": "Scan completed: nginx:1.25",
  "message": "Found 3 critical, 12 high, 47 medium vulnerabilities.",
  "url": "https://harborguard.co/dashboard/scans/scan-2k3j4",
  "timestamp": "2026-04-26T14:21:08.512Z",
  "scanId": "scan-2k3j4",
  "image": "nginx:1.25",
  "registry": "docker-hub",
  "engines": ["trivy", "grype", "syft", "dockle"],
  "durationMs": 18432,
  "counts": { "CRITICAL": 3, "HIGH": 12, "MEDIUM": 47, "LOW": 81 }
}

sla_breach

{
  "event": "sla_breach",
  "severity": "HIGH",
  "title": "SLA breach: CVE-2025-12345 on nginx:1.25",
  "message": "Critical CVE has exceeded the 7-day remediation deadline by 2 days.",
  "url": "https://harborguard.co/dashboard/vulnerabilities/CVE-2025-12345",
  "timestamp": "2026-04-26T14:21:08.512Z",
  "vulnerabilityId": "vuln-9001",
  "cveId": "CVE-2025-12345",
  "image": "nginx:1.25",
  "slaDeadline": "2026-04-19T00:00:00.000Z",
  "breachedBy": "P2DT" ,
  "assignee": "u-42"
}

cve_watch_kev

{
  "event": "cve_watch_kev",
  "severity": "CRITICAL",
  "title": "Tracked CVE added to CISA KEV: CVE-2025-12345",
  "message": "CVE-2025-12345 is now in the Known Exploited Vulnerabilities catalog. 4 of your images are affected.",
  "url": "https://harborguard.co/dashboard/cve-watch/CVE-2025-12345",
  "timestamp": "2026-04-26T14:21:08.512Z",
  "cveId": "CVE-2025-12345",
  "kevAddedAt": "2026-04-26T00:00:00.000Z",
  "affectedImages": ["nginx:1.25", "api:v2.4.0", "worker:latest", "ml-runner:cuda12"]
}

Signature verification

Compute HMAC-SHA256 over the raw request body using the channel's signing secret. Compare the result to the hex value in X-HarborGuard-Signature after stripping the sha256= prefix. Use a constant-time comparison to avoid timing attacks. Verify before parsing JSON; never trust an unverified payload.

import { createHmac, timingSafeEqual } from "node:crypto"
 
export function verifyHarborGuardSignature(
  rawBody: string,
  headerValue: string | undefined,
  secret: string,
): boolean {
  if (!headerValue?.startsWith("sha256=")) return false
  const expected = createHmac("sha256", secret).update(rawBody).digest("hex")
  const provided = headerValue.slice("sha256=".length)
  const a = Buffer.from(expected, "hex")
  const b = Buffer.from(provided, "hex")
  return a.length === b.length && timingSafeEqual(a, b)
}
 
// Express example — note express.raw() to keep the body verbatim
app.post(
  "/hooks/harborguard",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const raw = req.body.toString("utf8")
    const sig = req.header("x-harborguard-signature")
    if (!verifyHarborGuardSignature(raw, sig, process.env.HG_WEBHOOK_SECRET!)) {
      return res.status(401).end()
    }
    const event = JSON.parse(raw)
    // handle event...
    res.status(200).end()
  },
)

Acknowledgement and retries

HarborGuard treats any 2xx status as a successful delivery. Anything else — 4xx, 5xx, network error, TLS failure, or timeout (>10 s) — is a failure.

AspectValue
Success codes200, 201, 202, 204
Retry policyExponential backoff at 30 s, 2 m, 10 m, 1 h
Max attempts5
Per-endpoint circuit breakerOpens after 5 failures in 60 s, cools down for 60 s

When the circuit breaker is open, deliveries to that endpoint are short-circuited as failed without a network call. The breaker probes again after the cooldown.

Every delivery (success or failure) is logged with status code, response time, and error message. View the history in Settings -> Notifications -> [channel] -> Deliveries.

Idempotency

Some events fire from multiple producers (e.g. a CVE detected by both Trivy and Grype). HarborGuard deduplicates at the source so you receive one event per (image, CVE), but retries can cause your endpoint to see the same event body twice. Treat your handler as idempotent: key on (event, timestamp, target) or include your own dedupe table.

Rotating the signing secret

  1. Generate a new secret in your secret manager.
  2. Update the channel in HarborGuard: Settings -> Notifications -> [channel] -> Edit -> Signing secret.
  3. The very next delivery uses the new secret. There is no overlap window.

To rotate without downtime, run two channels (old and new URL or old and new secret) for a brief overlap, then delete the old channel.

HarborGuard does not currently publish a static IP allowlist for outbound webhook traffic. Authenticate by HMAC signature, not by source IP.

On this page