Webhooks push events from Hyperproof TPRM Core to an HTTPS endpoint you register, so you do not have to poll. Endpoints are registered from the Hyperproof app (ask your organization admin), and each endpoint subscribes to a list of event types.
Webhooks announce changes made inside Hyperproof — by users in the app, or by Hyperproof's own systems (for example, a completed monitoring run or a finished export). Changes you make through this API are not echoed back as webhooks: your integration already knows about its own writes, and an echo would create a loop. If you run several integrations against one organization, let them share state with each other directly instead of listening for each other's API writes.
Subscribe by exact event name — the live list is always at GET /manage/webhooks/events. The catalog by family:
- Vendors —
vendor.created,vendor.updated(carries only what changed:data.changesmaps each field to itspreviousandcurrentvalue),vendor.archived,vendor.risk_rating_changed. - Assessments —
assessment.created(fires when the creation wizard is finalized, never for drafts),assessment.status_changed(any status move — completion arrives here asdata.status: "Completed", reopening as"InProgress"),assessment.ai_started(AI analysis began processing the assessment), andassessment.submitted(an external collaborator submitted a module;data.moduleisquestionnaire,followup, ordocuments). - Questions —
question.marked_done(data.kindisquestionorfollowup). - Risks —
risk.created(a risk was added to the register from an assessment question;data.sourceisai_suggestedormanual, anddata.riskcarries the created risk's fields). - Collaborators —
collaborator.added,collaborator.updated. - Monitoring —
monitoring.run_started,monitoring.run_completed,monitoring.run_failed. - Files —
document.uploaded(ids are ready forGET /downloads/document/{id}) andexport.completed.
- Process deliveries idempotently. In normal operation, every event is sent exactly once. Only a failure recovery on our side can cause the same delivery to be sent again, and it always carries the same event
idandX-Hyperproof-Delivery-Id. If you key your processing on either id, a re-send becomes a harmless no-op. - Best-effort ordering. Events usually arrive in order, but retries and concurrent workers can reorder them. Sort by the
occurred_atfield in the envelope, never by arrival time. - Timeout. Your endpoint has 30 seconds to respond. Respond with any
2xxas quickly as you can; if your processing is slow, queue the work internally and respond first.
We retry a delivery after a 5xx response, a 429, or a network failure — up to 5 attempts, with backoff of 0s, 30s, 2m, 10m, 1h. Any other 4xx response is treated as permanent and is not retried. After 10 consecutive permanent failures, the endpoint is deactivated automatically. Once your URL is healthy again, re-enable the endpoint from the Hyperproof app; a live verification ping is required.
{
"id": "evt_…",
"type": "vendor.updated",
"api_version": "v1",
"occurred_at": "2026-07-12T09:30:00+00:00",
"source": "ui",
"data": { "…": "…" },
"delivery_id": "whd_…"
}Entity ids inside data are the same opaque ids the REST API returns, so you can feed them straight back into API calls.
Every delivery is signed, so you can prove it came from Hyperproof:
| Header | Value |
|---|---|
X-Hyperproof-Timestamp | Unix seconds at send time. |
X-Hyperproof-Signature | One or more v1=<hex> values, comma-separated. |
X-Hyperproof-Event | The event type. |
X-Hyperproof-Event-Id / X-Hyperproof-Delivery-Id | Correlation ids. |
Each signature is HMAC-SHA256(secret, "{timestamp}.{raw_body}"). Compute it with your endpoint's signing secret, and accept the delivery only if it matches any of the presented signatures (use a constant-time comparison) and the timestamp is within your tolerance (we recommend 5 minutes).
import hashlib, hmac
def verify(secret: str, timestamp: str, raw_body: bytes, header: str) -> bool:
expected = hmac.new(
secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256
).hexdigest()
return any(
hmac.compare_digest(sig.partition("=")[2], expected)
for sig in header.split(",")
)Why can the header carry two signatures? After you rotate an endpoint's signing secret, deliveries are co-signed with the old and the new secret for 24 hours, so you can switch keys on your side without dropping events. During that window the header looks like this:
X-Hyperproof-Signature: v1=5257a869e7ec…08d8bd,v1=9f31c04ab27e…d341aaThe first value is signed with the current (new) secret; the second with the secret being rotated out. You never need to work out which is which: compute your HMAC with the one secret you hold and accept the delivery if it equals any of the presented values — before you switch keys, yours matches the second; after you switch, it matches the first. Your verification code never changes. Once the 24-hour overlap ends, the header carries a single signature again.