Webhooks
85+ event types · HMAC-SHA-256 signed envelopes · 5-step retry schedule · dead-letter queue.
Overview
ReguNav’s webhook dispatcher emits compliance-domain events from the platform’s outbox to subscriber URLs the moment they happen. The same machinery that records an event in the audit-trail fans it out to your registered endpoints. Inbound webhooks (Stripe, customer-side, and cloud event-bus pushes from AWS SNS / Azure Event Grid / GCP Pub/Sub / Cloudflare Logpush) are handled by the same worker atwebhooks.regunav.com.
Outbound delivery
Endpoint & method
The dispatcher POSTs JSON to every subscriber URL configured for an event type. Bodies are CloudEvents-1.0-compatible envelopes.
Headers
| Header | Description |
|---|---|
content-type | application/json |
user-agent | ReguNav-Webhooks/1.0 |
x-regunav-signature | HMAC envelope: t=<unix_ts>,v1=<hex_sha256> |
x-regunav-event | The event name (e.g. tenant.created) |
x-regunav-tenant | Tenant ID the event was emitted for |
x-regunav-delivery-attempt | Integer attempt number (1 on first try; max 5) |
Signature verification
Reject any request whose signature does not match. The construction is identical to Stripe’s scheme so existing Stripe-verification helpers port directly:
signed_payload = "{timestamp}.{raw_request_body}"
expected_v1 = HMAC-SHA256(signed_payload, your_endpoint_secret)
header = "t={timestamp},v1={expected_v1}"Reference verifier (Node.js / Workers / Bun — crypto.subtle):
async function verifyReguNavSignature(rawBody, header, secret, toleranceSec = 300) {
const m = header.match(/(?:^|,\s*)t=(\d+),\s*v1=([0-9a-f]{64})\b/i);
if (!m) return false;
const [, t, v1] = m;
if (Math.abs(Math.floor(Date.now() / 1000) - Number(t)) > toleranceSec) return false;
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
);
const buf = await crypto.subtle.sign(
"HMAC",
key,
new TextEncoder().encode(`${t}.${rawBody}`),
);
const expected = [...new Uint8Array(buf)]
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
let diff = 0;
for (let i = 0; i < expected.length; i++) diff |= expected.charCodeAt(i) ^ v1.charCodeAt(i);
return diff === 0;
}Always verify against the raw request body, not a JSON round-tripped copy — key ordering and whitespace will change the digest.
Retries & DLQ
A delivery is considered successful when your endpoint returns a 2xx within the worker request budget. Anything else (non-2xx, network error, timeout) is retried on this schedule (seconds since the previous attempt):
- Attempt 2 — 1s after attempt 1
- Attempt 3 — 5s after attempt 2
- Attempt 4 — 30s after attempt 3
- Attempt 5 — 5min after attempt 4
- Final — 1h after attempt 5
After 5 failed attempts the event is parked in the dead-letter queue. The DLQ is operator-visible from the admin console (admin.regunav.com → Audit-trail → Webhooks) and can be replayed manually. Events stay in the audit-trail unconditionally — the outbox is purely for fan-out.
Idempotency
Each delivery carries a stable event_id in the body. Treat retries as idempotent — persist the event_id on your side and skip duplicates. ReguNav guarantees at-least-once delivery, not exactly-once.
Event envelope
{
"specversion": "1.0",
"id": "evt_01HK8A2…",
"source": "https://api.regunav.com/v1/tenants/tnt_…",
"type": "tenant.created",
"datacontenttype": "application/json",
"time": "2026-05-25T11:42:03.612Z",
"subject": "tnt_acme",
"tenantid": "tnt_acme",
"data": {
"tenant_id": "tnt_acme",
"display_name":"Acme Corp",
"plan": "Growth",
"region": "EU"
}
}Event categories
The canonical event catalog lives atpackages/engines/src/webhook-events.ts — it is the single source of truth for what the platform emits. Categories at the time of writing:
tenant.*ai_system.*framework.* / control.*evidence.*fria.*assessment.* / audit.*incident.*dsar.*vendor.*agent.*report.*policy.*training.*gpai_summary.*red_team.*hierarchybilling.*kye.*Inbound endpoints
Stripe — POST webhooks.regunav.com/stripe
Verifies the stripe-signature header per Stripe’s spec (5-minute skew window, timing-safe HMAC-SHA256 comparison). Configure the endpoint URL and webhook secret in your Stripe Dashboard; the secret is read from theSTRIPE_WEBHOOK_SECRET environment binding.
Customer-side — POST webhooks.regunav.com/inbound/{tenantId}
For pushing events into ReguNav from your own systems. Per-tenant HMAC verification via the regunav-signature header. The receiver classifies each payload through the event-registry and lands it on the canonical event-kind for downstream processing.
Cloud event-bus — POST webhooks.regunav.com/cloud/{provider}/{tenantId}
Native receivers for AWS SNS message-signed deliveries, Azure Event Grid withaeg-sas-key validation, GCP Pub/Sub with OIDC JWT verification, and Cloudflare Logpush HMAC. This is the BYOC (bring-your-own-cloud) path — no customer credentials at rest.
Discovery endpoints
The public API exposes three machine-discoverable indexes so SDKs and downstream consumers can enumerate what’s available without scraping the docs.
GET api.regunav.com/v1/glossary— 760+ regulatory terms across all 24 framework dictionaries, with their crosswalk edges. Use this to power autocomplete and regulator-language explainers.GET api.regunav.com/v1/search/kinds— the 20 searchable resource kinds backing native BM25 search at/v1/search. Returns the kind id, label, and indexed fields per kind.GET api.regunav.com/v1/reports/types— the 13 stakeholder-report types you can render viaPOST /v1/reports/generate. Each entry carries the stakeholder audience, the PDF template id, and required input fields.
Subscribing
Subscriptions are managed through the standard rail atapi.regunav.com/v1/webhook-subscriptions — see the API reference for the full create / list / rotate-secret / delete shape. The TypeScript SDK exposes the same operations under regunav.webhookSubscriptions.*.
Operational notes
- The dispatcher runs on Cloudflare Workers with a 30-second per-attempt budget; long-lived endpoints (>5s p99) should ACK quickly and process asynchronously.
- Outbound IPs are not stable — rely on signature verification, not allow-listing.
- Delivery state is observable at
GET /v1/webhook-deliveries(per-tenant) and in the admin audit-trail (cross-tenant). - Replays are explicit operator actions; ReguNav never replays automatically once an event has been parked.