Webhook Security
Each webhook has two secret families: one for publishing into the public ingest endpoint and one for verifying outbound deliveries. Hooksbase also keeps a secret-version ledger so you can rotate safely without breaking producers or consumers instantly.
Secret types
- Name
Ingest secret- Type
- whsec_...
- Description
Used only for calls into
/v1/ingest/{publicId}.
- Name
Signing secret- Type
- sws_...
- Description
Used only to verify the signature Hooksbase attaches to outbound delivery requests.
- Name
Secret versions- Description
Hooksbase stores lifecycle metadata for both secret families and exposes the non-secret version history through
/secret-versions.
Auth model
Project API keys manage webhook security surfaces. The dashboard mirrors the same controls with extra product affordances such as reveal and audit-log views.
Related Public API routes:
-
POST /v1/webhooks/{id}/rotate-ingest-secret -
POST /v1/webhooks/{id}/rotate-signing-secret -
GET /v1/webhooks/{id}/secret-versions -
POST /v1/webhooks/{id}/test-delivery
Rotation behavior
Rotation is overlap-aware:
- the new version becomes
currentimmediately - the previous current version becomes
overlapping - older versions become
retired
During overlap:
- both ingest secrets are accepted for public ingest
- outbound signatures can include both the current and overlapping signing secret
Only create and rotate responses return raw secret material. List and get routes never do.
Verifying outbound signatures
Hooksbase signs the exact raw body bytes it sends to your destination. The signing input is:
{deliveryId}.{unixTimestampSeconds}.<raw body bytes>
The HMAC is SHA-256, base64-encoded, and emitted as
v1,<base64>. Headers on the outbound request:
webhook-id— the delivery ID (also used as the signing input)webhook-timestamp— Unix timestamp in secondswebhook-signature— one or morev1,...values joined by spaces during a signing-secret overlap window
Use the SDK helper when possible:
Verify an outbound delivery
import { verifyHooksbaseWebhook } from '@hooksbase/sdk'
verifyHooksbaseWebhook({
headers: request.headers,
rawBody: await request.arrayBuffer(),
signingSecret: [
process.env.HOOKSBASE_CURRENT_SIGNING_SECRET!,
process.env.HOOKSBASE_PREVIOUS_SIGNING_SECRET!,
],
})
The helper enforces a 5-minute timestamp tolerance by default (override with
toleranceSeconds) and checks every v1,... candidate
signature against every provided secret, so rotation overlap is handled
automatically. It throws if no signature matches or the timestamp is too far
out of tolerance.
Dashboard-only views
Some security views exist only in the dashboard and are not part of the Public API:
- reveal the current signing secret for a webhook
- inspect the secret audit log
- rotate secrets from the UI
- run a synchronous test delivery without creating delivery history
Test delivery is also exposed on the Public API at
POST /v1/webhooks/{id}/test-delivery. It is
intentionally narrower than public ingest:
- it evaluates saved route rules against the probe payload, empty request headers, and derived provider metadata, falling back to the default destination when no rule matches
- it applies the saved transform, selected destination config, timeout, and usable signing secrets
- it does not create deliveries, emit analytics events, or raise operator incidents
- archived webhooks return
409, but paused webhooks are still testable - repeated probes are rate-limited so the endpoint is not a general-purpose synchronous traffic path
Common mistakes
- Verifying against parsed JSON instead of raw bytes.
- Forgetting that both signatures can be valid during an overlap window.
- Expecting list or get routes to ever return the raw secret again after creation.
- Treating test delivery as a full public-ingest routing test. Use public ingest when you need producer auth, provider verification, real request headers, and route evaluation end to end.