Webhooks
Open Saasframe provides a built-in webhook system compliant with the Standard Webhooks specification. The webhooks module enables:
- Outbound webhooks — push domain events to external HTTP endpoints
- Inbound webhooks — receive and verify webhooks from external systems via a generic adapter pattern
How It Works
Outbound Flow
When any module emits a domain event (e.g., customers.person.created, sales.order.fulfilled), the webhooks module:
- A persistent wildcard subscriber (
*) catches all platform events - Matches each event against registered webhook subscriptions (supports wildcards like
customers.*) - Enqueues delivery jobs to a dedicated
webhook-deliveriesqueue - A delivery worker signs the payload per Standard Webhooks spec and delivers it
- On failure, retries with exponential backoff (up to 10 attempts over ~24 hours)
Module emits event → Event Bus → Webhook Dispatcher → Delivery Queue → HTTP POST
Standard Webhooks Headers
Every outbound delivery includes these headers per the specification:
webhook-id: msg_2KWPBgLlAfxdpx2AI54pPJ85f4W
webhook-timestamp: 1674087231
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
content-type: application/json
Signature format: v1,<base64_hmac_sha256> of {webhook-id}.{webhook-timestamp}.{body}
Payload Structure
{
"type": "customers.person.created",
"timestamp": "2026-03-04T10:30:00.000Z",
"data": {
"id": "uuid-here",
"firstName": "John",
"lastName": "Doe",
"tenantId": "tenant-uuid",
"organizationId": "org-uuid"
}
}
Webhook Management
Integration-level controls
The module also registers a Custom Webhooks integration in the Integration Marketplace. That integration acts as the global control plane for the webhook subsystem:
- The integration State switch enables or blocks outbound deliveries, retries, test sends, and inbound receives.
- The integration Settings tab exposes quick links to
/backend/webhooks, a create action, and the Notify admins on failed delivery setting. - The integration Logs tab shows an aggregated delivery log across all configured webhook endpoints.
This means webhook operations have two scopes:
- Integration scope — global on/off and shared notification behavior.
- Endpoint scope — per-webhook URL, event patterns, retries, activation state, and secret lifecycle.
Creating a Webhook
Via API:
POST /api/webhooks
{
"name": "My Integration",
"url": "https://example.com/webhook",
"subscribedEvents": ["customers.*", "sales.order.created"],
"httpMethod": "POST"
}
The response includes a whsec_-prefixed signing secret. This is the only time the secret is returned in plaintext — store it securely.
Event Subscriptions
Webhooks support pattern-based event subscriptions:
| Pattern | Matches |
|---|---|
customers.person.created | Exact match only |
customers.* | All events in the customers namespace |
* | All platform events |
API Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/webhooks | List webhooks |
POST | /api/webhooks | Create webhook |
GET | /api/webhooks/:id | Get webhook details |
PUT | /api/webhooks/:id | Update webhook |
DELETE | /api/webhooks/:id | Soft-delete webhook |
POST | /api/webhooks/:id/rotate-secret | Rotate signing secret |
POST | /api/webhooks/:id/test | Send test delivery |
GET | /api/webhooks/deliveries | List delivery logs |
POST | /api/webhooks/deliveries/:id/retry | Retry failed delivery |
GET | /api/webhooks/events | List available event types |
Verifying Webhooks (Consumer Guide)
If you're building a service that receives webhooks from Open Saasframe, verify the signature to ensure authenticity:
import { createHmac, timingSafeEqual } from 'node:crypto'
function verifyWebhook(headers: Record<string, string>, body: string, secret: string): boolean {
const webhookId = headers['webhook-id']
const timestamp = headers['webhook-timestamp']
const signature = headers['webhook-signature']
// Check replay window (5 minutes)
const now = Math.floor(Date.now() / 1000)
if (Math.abs(now - parseInt(timestamp)) > 300) return false
// Verify signature
const toSign = `${webhookId}.${timestamp}.${body}`
const secretBytes = Buffer.from(secret.replace('whsec_', ''), 'base64')
const expected = createHmac('sha256', secretBytes).update(toSign).digest('base64')
// Extract signature value (after "v1,")
const received = signature.split(' ').find(s => s.startsWith('v1,'))?.slice(3)
if (!received) return false
return timingSafeEqual(Buffer.from(expected), Buffer.from(received))
}
Use the official Standard Webhooks libraries for verification in production — they handle edge cases like key rotation and multiple signatures.
Retry Behavior
Failed deliveries are retried with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | ~5 seconds |
| 3 | ~5 minutes |
| 4 | ~30 minutes |
| 5 | ~2 hours |
| 6 | ~5 hours |
| 7 | ~10 hours |
| 8-10 | ~14-24 hours |
Non-retryable responses: HTTP 4xx (except 408 and 429) are not retried.
Auto-disable: After a configurable number of consecutive failures (default: 100), the webhook is automatically disabled. HTTP 410 (Gone) responses immediately disable the webhook.
Key Rotation
Rotate webhook secrets without downtime:
POST /api/webhooks/:id/rotate-secret
During the rotation period (default: 24 hours):
- Outbound deliveries are dual-signed with both the new and old secret
- Consumers can verify with either secret
- After the rotation period, the old secret is automatically removed
Secret Format
Webhook secrets use the Standard Webhooks format:
- Prefix:
whsec_ - Content: Base64-encoded random bytes (minimum 24 bytes / 192 bits)
- Example:
whsec_h4fk4iWceEsgYw/JbT9Feg98sgSFYsAy
Delivery Strategies
| Strategy | Use Case | Status |
|---|---|---|
http | Standard HTTP/HTTPS endpoints (default) | Available today |
sqs | AWS SQS queues for async processing | Planned |
sns | AWS SNS topics for fan-out | Planned |
Inbound Webhooks
The webhooks module provides a generic receiver for external webhook sources:
POST /api/webhooks/inbound/:endpointId
Third-party modules can register WebhookEndpointAdapter implementations to handle specific providers (e.g., Stripe, Zapier). The adapter contract:
interface WebhookEndpointAdapter {
readonly providerKey: string
readonly subscribedEvents: string[]
formatPayload(event): Promise<WebhookPayload>
verifyWebhook(input): Promise<InboundWebhookEvent>
processInbound(event): Promise<void>
}
Inbound webhooks are:
- Rate limited per endpoint
- Deduplicated by message ID
- Processed via fire-and-forget (return 200 immediately, emit event for async processing)
Shared Primitives
The webhook signing, verification, and secret management utilities are available as a shared library for use by any module:
import {
signPayload,
buildStandardHeaders,
verifyStandardWebhook,
generateWebhookSecret,
parseWebhookSecret,
} from '@saasframe/shared/lib/webhooks'
Admin UI
The webhook management dashboard is available at /backend/webhooks with:
- A webhook list with status, event patterns, last delivery, and quick actions
- Create/edit forms with event selection, retry controls, timeout, rate limiting, and custom headers
- One-time secret reveal after create or rotate
- Per-endpoint detail pages with test sends, secret rotation, and delivery drilldown
- Aggregated delivery visibility from the integration detail page at
/backend/integrations/webhook_custom
Primary operator routes
| Route | Purpose |
|---|---|
/backend/integrations/webhook_custom | Global webhook settings, failed-delivery notifications, configured endpoint shortcuts, aggregated delivery log |
/backend/webhooks | Endpoint list and create entry point |
/backend/webhooks/create | Create a new webhook endpoint |
/backend/webhooks/:id | Endpoint detail, send test, rotate secret, inspect delivery history |
Failed delivery notifications
When Notify admins on failed delivery is enabled in the integration Settings tab, the module creates in-app notifications for admin-capable users after a delivery exhausts all retry attempts. This notification layer is independent from HTTP retry logic and is meant to surface operational issues early.
Secret lifecycle UX
Secrets are generated automatically and revealed in plaintext only:
- immediately after webhook creation
- immediately after secret rotation
After that, the admin UI only shows a masked value. Operators must copy the secret during the reveal window or rotate it again later.
RBAC Features
| Feature | Description |
|---|---|
webhooks.view | View webhooks and delivery logs |
webhooks.manage | Create, edit, and delete webhooks |
webhooks.secrets | View and rotate webhook secrets |
webhooks.test | Send test webhook deliveries |
Related
- User Guide: Custom Webhooks — operator-facing walkthrough with screenshots
- Events & Subscribers — the event system that powers webhook dispatching
- Queue & Workers — the queue infrastructure used for delivery processing
- SPEC-057 — Webhooks Module — full technical specification