Webhooks
Webhooks deliver outbound notifications from Novantra to your integration when important state changes happen in your workspace. They let you avoid polling list endpoints just to discover new findings, completed assessments, or submission events.
Event catalogue
The v1 webhook surface covers the workspace state changes external integrations most often need:
| Event | When it fires |
|---|---|
governance.finding.created.v1 | A new finding was recorded in your workspace (via the UI or POST /api/v1/governance/findings). |
governance.evidence_export.ready.v1 | An evidence export your workspace requested has finished and is ready to retrieve. |
Always read the live catalogue from GET /api/v1/webhooks/events and subscribe to
the event types it returns; that endpoint is the authoritative list for your
workspace.
Payload shape
Every webhook delivery is a POST to your configured URL with this JSON body:
{
"id": "evt_01HXY...",
"type": "governance.finding.created.v1",
"createdAt": "2026-05-21T12:34:56.789Z",
"organization": {
"id": "org_01H...",
"slug": "acme"
},
"data": {
"finding": {
"id": "find_01H...",
"title": "Annual control review overdue",
"severity": "medium",
"status": "open",
"subjectModuleKey": "controls",
"subjectResourceType": "control",
"subjectResourceId": "ctrl_01H...",
"createdAt": "2026-05-21T12:34:56.123Z"
}
}
}Fields you can rely on across every event type:
id- stable event ID. Use it for replay-safe deduplication.type- the event type. Branch on this.createdAt- when the event happened in the workspace.organization- which workspace it came from (always set).data- the event-specific payload. The shape insidedatamatches the public resource shape for that event type.X-Novantra-Delivery-Id- unique delivery attempt ID for delivery support and audit review.
Signature verification
Every webhook delivery must be verified. Without verification, an attacker who learns your webhook URL could impersonate Novantra and forge events.
Each delivery includes:
X-Novantra-Signature: t=1779366896,v1=<signature>
X-Novantra-Timestamp: 2026-05-21T12:34:56.789Z
X-Novantra-Delivery-Id: whd_01EXAMPLE00000000000000000
X-Novantra-Event: governance.finding.created.v1To verify:
- Concatenate the timestamp and the raw request body with a
.separator:<timestamp>.<body>. - Compute
HMAC-SHA256of that string, using your webhook’s signing secret as the key. - Compare the hex digest to the
v1=value in theX-Novantra-Signatureheader. Use a constant-time comparison. - Reject the delivery if the digest doesn’t match.
- Reject the delivery if the timestamp is older than 5 minutes (replay defense).
Pseudocode:
def verify(headers, raw_body, signing_secret):
signature = parse_v1(headers["X-Novantra-Signature"])
timestamp = headers["X-Novantra-Timestamp"]
if not within_5_minutes(timestamp):
return False
expected = hmac.new(
signing_secret.encode(),
f"{timestamp}.{raw_body}".encode(),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)Compute the HMAC over the raw request body, exactly as sent. Re-serializing the JSON before hashing breaks verification because key order and whitespace matter.
Subscribing
Create a webhook subscription for a service account from Developers -> Webhooks -> Endpoints, or programmatically with a token that has the webhooks:manage scope:
POST /api/v1/webhooks
Authorization: Bearer <access-token>
Content-Type: application/json
{
"url": "https://example.com/novantra/webhook",
"events": ["governance.finding.created.v1"],
"description": "ServiceNow incident sync",
"reason": "Wire findings into ServiceNow incident queue."
}Response:
{
"webhook": {
"id": "whe_01EXAMPLE00000000000000000",
"name": "ServiceNow incident sync",
"urlHost": "example.com",
"subscribedEvents": ["governance.finding.created.v1"],
"status": "active",
"createdAt": "2026-05-21T12:34:56.789Z",
"updatedAt": "2026-05-21T12:34:56.789Z"
},
"signingSecret": "<webhook-signing-secret>"
}The signingSecret is returned only once, at creation time. Store it in your secrets store. You can rotate it later, but you cannot retrieve it.
Subscribe to the specific event types your receiver is designed to process. Use GET /api/v1/webhooks/events to refresh the current event catalogue.
Signing-secret posture
Developers -> Webhooks shows safe signing-secret posture for each endpoint:
- storage posture;
- one-time reveal posture;
- last rotation time;
- next rotation due date, when configured;
- last verification time, when available;
- whether customer action is required.
This posture helps administrators identify endpoints that need receiver updates after creation or rotation. It never exposes the webhook signing secret after the one-time create or rotate response, and idempotency replays return only the webhook record.
Novantra evaluates this posture from safe rotation metadata and delivery evidence. Depending on your configured rotation policy, an endpoint can move from current to due soon, overdue, or customer-action-required without exposing the signing secret.
Managing endpoints
Webhook endpoint changes require the webhooks:manage scope and an Idempotency-Key header.
Update a webhook’s receiver metadata, URL, or event subscriptions:
PATCH /api/v1/webhooks/whe_01EXAMPLE00000000000000000
Authorization: Bearer <access-token>
Content-Type: application/json
Idempotency-Key: webhook-update-2026-05-21
{
"name": "ServiceNow incident sync",
"url": "https://example.com/novantra/webhook",
"events": ["governance.finding.created.v1"],
"description": "ServiceNow incident queue receiver",
"reason": "Refresh the approved receiver endpoint and subscriptions."
}Pause or resume delivery without changing subscriptions:
POST /api/v1/webhooks/whe_01EXAMPLE00000000000000000/pause
Authorization: Bearer <access-token>
Content-Type: application/json
Idempotency-Key: webhook-pause-2026-05-21
{
"reason": "Pause delivery while the receiver is under maintenance."
}Rotate the signing secret when your credential policy requires it:
POST /api/v1/webhooks/whe_01EXAMPLE00000000000000000/rotate-secret
Authorization: Bearer <access-token>
Content-Type: application/json
Idempotency-Key: webhook-rotate-2026-05-21
{
"reason": "Rotate the webhook signing secret under the scheduled credential policy."
}The rotation response returns a new signingSecret once. Idempotency replays return the webhook record without exposing the secret again.
Delete a webhook when the receiver is no longer approved:
DELETE /api/v1/webhooks/whe_01EXAMPLE00000000000000000
Authorization: Bearer <access-token>
Content-Type: application/json
Idempotency-Key: webhook-delete-2026-05-21
{
"reason": "Receiver endpoint is no longer approved for this integration."
}Delivery semantics
- At-least-once. A given event may be delivered more than once if your endpoint is briefly unavailable or returns a non-2xx status. Use the event
idto deduplicate. - Ordering is best-effort within an event type. Cross-type ordering is not guaranteed.
- Timeouts. Your endpoint must respond within 10 seconds with a
2xxstatus. Otherwise the delivery is considered failed and queued for retry. - Synchronous vs asynchronous. Acknowledge with
2xxfirst, then process. Long-running processing should happen in a background queue, not in the request handler.
Retries
Failed deliveries are retried with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 (initial) | immediate |
| 2 | 1 minute |
| 3 | 2 minutes |
| 4 | 4 minutes |
| 5 | 8 minutes |
| 6 | 16 minutes |
| 7 | 32 minutes |
After 7 failed attempts, the delivery is marked dead-lettered. The webhook itself is not disabled, but the workspace surface shows a delivery failure that an admin can review and trigger manual replay.
Replay
Workspace admins can replay any past delivery from the webhook detail page.
A replayed delivery uses the same payload as the original; only X-Novantra-Delivery-Id changes. Your deduplication should key on event id (which is stable across replays) rather than the delivery ID header.
Disabling and rotating
- Pause a webhook to temporarily stop deliveries without losing the subscription. New events accumulate and are delivered when the webhook is unpaused.
- Rotate signing secret to invalidate the current secret. A grace window of 60 seconds keeps the old secret valid so you can update your integration’s stored secret without a window of failed verifications.
- Delete a webhook to permanently end the subscription. Pending deliveries for the deleted endpoint are no longer delivered.
What is and isn’t a webhook event
| Webhook event? | |
|---|---|
| Customer-impacting state changes (find created, evidence approved, etc.) | yes |
| Background system events (job runs, posture sweeps) | no |
| Admin-only events (license renewals, mailer config changes) | no |
| Reads | no - webhooks are write/state-change-driven only |
Compliance and audit
Every webhook delivery is recorded in the workspace audit log with its delivery ID, the event ID, and the destination URL. Webhook subscription changes (create, update, delete, rotate secret, pause) are also audited.
Next
- Governance reference - per-resource endpoints for what fires which event.