Webhooks
Webhooks push real-time events into your own systems when scans complete or violation state changes. Every payload is signed so your receiver can verify it came from AllyProof.
Supported events
| Event | Fires when |
|---|---|
scan.completed | Any scan job transitions to completed — manual, scheduled, or API-triggered. Payload carries the site, score, severity breakdown, and scan metadata. |
violation.new | A violation is detected for the first time on a site (no prior open or resolved row for the same rule). Fires once per violation lifetime — a re-scan that re-detects an already-known violation does NOT re-fire. Useful for ticketing integrations that want one row per defect. Payload carries violation_id, site_id, site_url, rule_id, impact, and first_detected_at. |
violation.resolved | A previously-open violation transitions to resolved. Payload carries resolution_source with one of three values: "scanner" (no longer detected by a fresh scan), "manual" (a user marked it resolved in the dashboard or via the REST API), or "jira" (the linked Jira issue moved into the Done category). The transition guard prevents double-emission when a re-update is a no-op. |
violation.regressed | A violation that was previously resolved reappeared in the latest scan. Fires once per regressed issue per scan. Payload carries the prior resolved_atso your integration can flag "fix reverted" timelines. |
Setup
- Open Settings → Notifications (admin or owner role).
- Enter your Webhook URL — the HTTPS endpoint that should receive events. AllyProof rejects plain HTTP and any URL that resolves to a private-range IP.
- Enter a webhook secret — any random string of sufficient length. This is the key used to sign every outbound payload. AllyProof does not auto-generate it; if the field is empty, webhook delivery is skipped entirely (the platform refuses to send unsigned payloads).
- Save.
Each org has one webhook URL and one secret. If you need to fan out to multiple destinations, point the URL at a lightweight fan-out worker on your side.
Payload format
All events POST a JSON body with the same top-level shape:
{
"event": "scan.completed",
"timestamp": "2026-04-24T09:12:34.567Z",
"data": {
"site_id": "…",
"site_url": "https://example.com",
"scan_id": "…",
"score": 87,
"summary": {
"critical": 0,
"serious": 2,
"moderate": 5,
"minor": 10,
"pages_scanned": 25
}
}
}The data object varies by event type. Treat unknown fields as forward-compatible extensions rather than breaking changes — new fields can appear without a version bump.
Signature verification
Every request includes the X-AllyProof-Signature header: the HMAC-SHA256 of the raw request body, keyed by your webhook secret, hex-encoded. Always verify the signature before acting on the payload — treat an unverifiable request exactly as if it never arrived.
import crypto from "node:crypto";
function verify(req) {
const signature = req.headers["x-allyproof-signature"];
const expected = crypto
.createHmac("sha256", process.env.ALLYPROOF_WEBHOOK_SECRET)
.update(req.rawBody, "utf8")
.digest("hex");
// Constant-time comparison guards against timing attacks.
const ok =
signature.length === expected.length &&
crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
if (!ok) throw new Error("invalid signature");
}Delivery guarantees
- Best-effort. AllyProof fires the HTTP request once. A non-2xx response is logged but not automatically retried — if your receiver is down, rely on re-reading the canonical data via the API when it comes back up.
- Non-blocking. A slow or unreachable webhook never blocks scan processing or affects the customer-facing UI. Delivery runs after the scan record is persisted.
- Signed only. If the secret is unset, delivery is refused. The platform never sends unsigned payloads.
Security
- Webhook URLs are validated against private-range IPs to prevent SSRF; internal targets (10/8, 172.16/12, 192.168/16, loopback) are rejected at save time.
- The secret is stored server-side and never returned through the API after you've set it. Rotate by replacing it in settings — both old and new receivers will need the new value.
Typical use cases
- Post scan results into a Slack channel via a webhook relay.
- Open Jira or Linear tickets on
violation.new. - Update an internal compliance dashboard on
scan.completed. - Trigger a remediation workflow in your PM tool.
- Page on-call when a critical-severity violation surfaces (pair with the scan-complete severity data).