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/webhooks

Returns 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/webhooks

Request Body:

{
  "url": "https://example.com/webhook",
  "secret": "your-webhook-secret",
  "events": ["database.event", "document.updated"]
}
FieldRequiredDescription
urlYesHTTPS endpoint URL
secretYesShared secret for HMAC-SHA256 signing
eventsYesArray of event types to deliver

Supported Event Types:

  • document.updated — Sync document updated
  • document.deleted — Sync document deleted
  • presence.updated — Presence change
  • socket.message — Socket message
  • socket.connect — Client connected
  • socket.disconnect — Client disconnected
  • database.event — Database CDC event

Activate / Deactivate

POST /api/webhooks/:id/activate
POST /api/webhooks/:id/deactivate

Deactivated endpoints stop receiving deliveries but are not deleted.

Delete Endpoint

DELETE /api/webhooks/:id

Delivery Logs

GET /api/webhooks/logs?event=database.event

Returns 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:

HeaderDescription
X-Webhook-SignatureSignature in t=<timestamp>,v1=<hex-encoded HMAC-SHA256> format
X-Webhook-TimestampSigning timestamp in milliseconds
X-Webhook-Endpoint-IdThe endpoint ID that matched this delivery
Content-Typeapplication/json

The signed content is ${timestamp}.${rawBody} — the timestamp concatenated with a dot and the raw JSON body. Including the timestamp prevents replay attacks.

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"
  }
}