{"templateId":"markdown","sharedDataIds":{"sidebar":"sidebar-sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":[]},"type":"markdown"},"seo":{"title":"Webhooks","description":"Hyperproof developer resources for custom integrations.","llmstxt":{"hide":false,"sections":[{"title":"Table of contents","includeFiles":["**/*"],"excludeFiles":[]}],"excludeFiles":[]}},"dynamicMarkdocComponents":[],"compilationErrors":[],"ast":{"$$mdtype":"Tag","name":"article","attributes":{},"children":[{"$$mdtype":"Tag","name":"Heading","attributes":{"level":1,"id":"webhooks","__idx":0},"children":["Webhooks"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["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."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"what-triggers-an-event","__idx":1},"children":["What triggers an event"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Webhooks announce changes made ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["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 ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["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."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"event-catalog","__idx":2},"children":["Event catalog"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Subscribe by exact event name — the live list is always at"," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["GET /manage/webhooks/events"]},". The catalog by family:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Vendors"]}," — ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["vendor.created"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["vendor.updated"]}," (carries only what changed:"," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["data.changes"]}," maps each field to its ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["previous"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["current"]}," value),"," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["vendor.archived"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["vendor.risk_rating_changed"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Assessments"]}," — ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["assessment.created"]}," (fires when the creation wizard is"," ","finalized, never for drafts), ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["assessment.status_changed"]}," (any status move —"," ","completion arrives here as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["data.status: \"Completed\""]},", reopening as"," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["\"InProgress\""]},"), ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["assessment.ai_started"]}," (AI analysis began processing the"," ","assessment), and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["assessment.submitted"]}," (an external collaborator submitted"," ","a module; ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["data.module"]}," is ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["questionnaire"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["followup"]},", or ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["documents"]},")."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Questions"]}," — ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["question.marked_done"]}," (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["data.kind"]}," is ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["question"]}," or"," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["followup"]},")."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Risks"]}," — ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["risk.created"]}," (a risk was added to the register from an"," ","assessment question; ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["data.source"]}," is ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ai_suggested"]}," or ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["manual"]},", and"," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["data.risk"]}," carries the created risk's fields)."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Collaborators"]}," — ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["collaborator.added"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["collaborator.updated"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Monitoring"]}," — ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["monitoring.run_started"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["monitoring.run_completed"]},","," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["monitoring.run_failed"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Files"]}," — ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["document.uploaded"]}," (ids are ready for"," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["GET /downloads/document/{id}"]},") and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["export.completed"]},"."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"delivery-semantics","__idx":3},"children":["Delivery semantics"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["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 ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["same"]}," event ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["id"]}," and"," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Hyperproof-Delivery-Id"]},". If you key your processing on either id, a"," ","re-send becomes a harmless no-op."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Best-effort ordering."]}," Events usually arrive in order, but retries and"," ","concurrent workers can reorder them. Sort by the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["occurred_at"]}," field in the"," ","envelope, never by arrival time."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Timeout."]}," Your endpoint has ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["30 seconds"]}," to respond. Respond with any"," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["2xx"]}," as quickly as you can; if your processing is slow, queue the work"," ","internally and respond first."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"retries","__idx":4},"children":["Retries"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["We retry a delivery after a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["5xx"]}," response, a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["429"]},", or a network failure — up"," ","to ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["5 attempts"]},", with backoff of ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["0s, 30s, 2m, 10m, 1h"]},". Any other ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["4xx"]}," ","response is treated as permanent and is not retried. After ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["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."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"the-envelope","__idx":5},"children":["The envelope"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"id\": \"evt_…\",\n  \"type\": \"vendor.updated\",\n  \"api_version\": \"v1\",\n  \"occurred_at\": \"2026-07-12T09:30:00+00:00\",\n  \"source\": \"ui\",\n  \"data\": { \"…\": \"…\" },\n  \"delivery_id\": \"whd_…\"\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Entity ids inside ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["data"]}," are the same opaque ids the REST API returns, so you"," ","can feed them straight back into API calls."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"verifying-signatures","__idx":6},"children":["Verifying signatures"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Every delivery is signed, so you can prove it came from Hyperproof:"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Header"},"children":["Header"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Value"},"children":["Value"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Hyperproof-Timestamp"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Unix seconds at send time."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Hyperproof-Signature"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["One or more ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v1=<hex>"]}," values, comma-separated."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Hyperproof-Event"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The event type."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Hyperproof-Event-Id"]}," / ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Hyperproof-Delivery-Id"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Correlation ids."]}]}]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Each signature is ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["HMAC-SHA256(secret, \"{timestamp}.{raw_body}\")"]},". Compute it"," ","with your endpoint's signing secret, and accept the delivery only if it"," ","matches ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["any"]}," of the presented signatures (use a constant-time comparison)"," ","and the timestamp is within your tolerance (we recommend 5 minutes)."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"python","header":{"controls":{"copy":{}}},"source":"import hashlib, hmac\n\ndef verify(secret: str, timestamp: str, raw_body: bytes, header: str) -> bool:\n    expected = hmac.new(\n        secret.encode(), f\"{timestamp}.\".encode() + raw_body, hashlib.sha256\n    ).hexdigest()\n    return any(\n        hmac.compare_digest(sig.partition(\"=\")[2], expected)\n        for sig in header.split(\",\")\n    )\n","lang":"python"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Why can the header carry two signatures?"]}," After you rotate an endpoint's"," ","signing secret, deliveries are co-signed with the old ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["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:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"X-Hyperproof-Signature: v1=5257a869e7ec…08d8bd,v1=9f31c04ab27e…d341aa\n"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["first"]}," value is signed with the current (new) secret; the ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["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 ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["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."]}]},"headings":[{"value":"Webhooks","id":"webhooks","depth":1},{"value":"What triggers an event","id":"what-triggers-an-event","depth":2},{"value":"Event catalog","id":"event-catalog","depth":2},{"value":"Delivery semantics","id":"delivery-semantics","depth":2},{"value":"Retries","id":"retries","depth":2},{"value":"The envelope","id":"the-envelope","depth":2},{"value":"Verifying signatures","id":"verifying-signatures","depth":2}],"frontmatter":{"seo":{"title":"Webhooks"}},"lastModified":"2026-08-11T17:44:00.000Z","pagePropGetterError":{"message":"","name":""}},"slug":"/hyperproof-tprm/webhooks","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}