Consume signed webhooks safely
Webhooks notify your system that a durable event is available. Delivery is at least once: verify every signature, deduplicate event IDs, and fetch authoritative resources from the API.
Create an endpoint
{
"url": "https://integrations.example.com/accesspreflight/webhooks",
"events": ["scan.completed", "scan.failed"]
}
Use an HTTPS endpoint. The API shows the signing secret once. Store it in a secret manager, not in source control.
Signature format
The AccessPreflight-Signature header has this form:
t=<unix-seconds>,v1=<hex-hmac-sha256>
The signed message is:
<timestamp>.<exact raw request body bytes>
Compute HMAC-SHA256 with the endpoint secret and compare the expected and received digests in constant time. Reject stale timestamps according to your replay window.
Node.js verification example
import { createHmac, timingSafeEqual } from 'node:crypto'
const verify = ({ rawBody, signatureHeader, secret, now = Date.now() }) => {
const fields = Object.fromEntries(
signatureHeader.split(',').map((part) => part.split('=', 2)),
)
const timestamp = Number(fields.t)
if (!Number.isSafeInteger(timestamp)) return false
if (Math.abs(Math.floor(now / 1000) - timestamp) > 300) return false
const expected = createHmac('sha256', secret)
.update(`${timestamp}.`)
.update(rawBody)
.digest()
const received = Buffer.from(fields.v1 || '', 'hex')
return (
received.length === expected.length && timingSafeEqual(received, expected)
)
}
Capture the raw bytes before JSON parsing. Re-serializing parsed JSON changes the signed payload.
Acknowledge quickly
- Verify timestamp and signature.
- Persist the event ID with a unique constraint.
- Return
2xx. - Process asynchronously.
- Fetch the referenced scan or delivery state through the API.
Webhooks carry IDs and summaries, not complete source or report content.
Whole-site scans emit one parent terminal event, never one event per page. Optional scan.progress milestones are bounded to discovery completion and 25%, 50%, and 75% of admitted page work. Fetch the parent scan for current counters; progress delivery may be duplicated or arrive after a newer parent state.
Delivery and replay
AccessPreflight retries failed deliveries for up to 24 hours. Duplicate deliveries are normal. A permanent failure pattern can disable an endpoint.
Use the delivery log to diagnose response codes and replay a specific delivery after fixing your consumer. Replays keep the event identity semantics, so the same deduplication rule remains safe.
next_attempt_at is populated only for pending and retrying deliveries. It
is null after delivered, terminal failed, or cancelled, so a terminal
row never implies another scheduled attempt. last_error_code is a stable,
sanitized diagnostic such as transport_timeout or http_status_503; target
URLs, credentials, and raw transport errors are not returned.
Disabling an endpoint stops future delivery attempts but retains endpoint, event, and delivery history for audit and support. It is not a deletion operation. Use the applicable data-deletion workflow when history must be removed under your retention policy.
Rotate with overlap
Secret rotation supports a 24-hour overlap. During the overlap, accept a valid signature from either secret, then remove the old secret from your verifier after the deadline.