API Reference
Webhooks API
Manage webhook endpoints, delivery logs, and HMAC-signed event dispatch.
The Webhooks API manages HTTP callback endpoints that receive real-time events. Webhooks are HMAC-signed for security and support automatic retries with exponential backoff.
Info
Webhooks subscribe to topics, not to tables or mappings. Any event that lands on a topic — from CDC, Sync, Socket, or SDK — is delivered to matching webhook endpoints. See Core Concepts for the full fan-out model.
List Endpoints
GET /api/webhooksReturns all webhook endpoints scoped to the current application.
Response:
[
{
"id": "wh_abc123",
"url": "https://example.com/webhook",
"events": ["database.event", "document.updated"],
"active": true,
"applicationId": "app_abc123",
"createdAt": 1710000000000
}
]Create Endpoint
POST /api/webhooksRequest Body:
{
"url": "https://example.com/webhook",
"secret": "your-webhook-secret",
"events": ["database.event", "document.updated"]
}| Field | Required | Description |
|---|---|---|
url | Yes | HTTPS endpoint URL |
secret | Yes | Shared secret for HMAC-SHA256 signing |
events | Yes | Array of event types to deliver |
Supported Event Types:
document.updated— Sync document updateddocument.deleted— Sync document deletedpresence.updated— Presence changesocket.message— Socket messagesocket.connect— Client connectedsocket.disconnect— Client disconnecteddatabase.event— Database CDC event
Activate / Deactivate
POST /api/webhooks/:id/activate
POST /api/webhooks/:id/deactivateDeactivated endpoints stop receiving deliveries but are not deleted.
Delete Endpoint
DELETE /api/webhooks/:idDelivery Logs
GET /api/webhooks/logs?event=database.eventReturns the delivery history with status, response codes, and retry counts.
Response:
[
{
"id": "log_abc123",
"webhookId": "wh_abc123",
"event": "database.event",
"url": "https://example.com/webhook",
"statusCode": 200,
"success": true,
"attempts": 1,
"deliveredAt": 1710000100000
}
]Webhook Signing
Every webhook delivery includes an HMAC-SHA256 signature for verification. The following headers are sent with each request:
| Header | Description |
|---|---|
X-Webhook-Signature | Signature in t=<timestamp>,v1=<hex-encoded HMAC-SHA256> format |
X-Webhook-Timestamp | Signing timestamp in milliseconds |
X-Webhook-Endpoint-Id | The endpoint ID that matched this delivery |
Content-Type | application/json |
The signed content is ${timestamp}.${rawBody} — the timestamp concatenated with a dot and the raw JSON body. Including the timestamp prevents replay attacks.
Using the SDK (Recommended)
The SDK includes a built-in webhook receiver that handles signature verification, replay protection, and payload parsing. See the Webhook Receiver SDK guide for full details.
import { WebhookReceiver } from '@smarterservices/realtime';
const receiver = new WebhookReceiver('whsec_your_secret');
app.post('/webhooks', express.text({ type: '*/*' }), (req, res) => {
try {
const { event } = receiver.verify(req.body, req.headers);
console.log('Verified event:', event.topic, event.type);
res.sendStatus(200);
} catch (err) {
res.sendStatus(400);
}
});Manual Verification
If you cannot use the SDK, verify signatures manually:
import { createHmac, timingSafeEqual } from 'node:crypto';
function verifyWebhook(
rawBody: string,
signatureHeader: string,
secret: string,
toleranceMs = 300_000
): boolean {
// Parse header: t=<timestamp>,v1=<signature>
const parts = signatureHeader.split(',');
const tPart = parts.find((p) => p.startsWith('t='));
const vPart = parts.find((p) => p.startsWith('v1='));
if (!tPart || !vPart) return false;
const timestamp = parseInt(tPart.slice(2), 10);
const receivedSig = vPart.slice(3);
// Reject expired timestamps (replay protection)
if (Math.abs(Date.now() - timestamp) > toleranceMs) return false;
// Compute expected signature
const expected = createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
// Constant-time comparison
if (expected.length !== receivedSig.length) return false;
return timingSafeEqual(Buffer.from(expected), Buffer.from(receivedSig));
}Retry Policy
Failed deliveries are retried automatically using BullMQ:
- Max retries: 5
- Backoff: Exponential with jitter
- Dead-letter queue: Failed deliveries after max retries are moved to a DLQ for manual inspection
Delivery Payload
{
"id": "evt_a1b2c3d4",
"topic": "session.status",
"source": "database",
"type": "database.update",
"payload": { "sessionId": "abc123", "status": "active" },
"metadata": {
"timestamp": 1710000000000,
"table": "sessions",
"operation": "update"
}
}