SDK
Webhook Receiver
Verify and parse incoming webhook deliveries from the Realtime Platform using the SDK's built-in receiver.
The SDK includes a server-side webhook receiver that validates HMAC-SHA256 signatures, checks for replay attacks, and returns a typed RealtimeEvent you can use directly in your application.
Info
This is a server-side utility. Use it in your Node.js backend to receive webhook deliveries — not in browser code. For client-side real-time events, use Subscribing to Topics instead.
Quick Start
import { WebhookReceiver } from '@smarterservices/realtime';
import express from 'express';
const app = express();
const receiver = new WebhookReceiver('whsec_your_endpoint_secret');
// Use raw body parsing so the signature can be verified against the exact bytes
app.post('/webhooks/realtime', express.text({ type: '*/*' }), (req, res) => {
try {
const { event } = receiver.verify(req.body, req.headers);
console.log('Received:', event.topic, event.type, event.payload);
res.sendStatus(200);
} catch (err) {
console.error('Verification failed:', err.message);
res.sendStatus(400);
}
});
app.listen(4100);WebhookReceiver
The class-based receiver. Create one instance per webhook endpoint secret and call verify() on every incoming request.
Constructor
new WebhookReceiver(secret: string, options?: WebhookReceiverOptions)| Parameter | Type | Required | Description |
|---|---|---|---|
secret | string | Yes | The shared secret for this webhook endpoint, as configured when creating the endpoint via the Webhooks API |
options | WebhookReceiverOptions | No | Optional configuration (see below) |
WebhookReceiverOptions:
| Field | Type | Default | Description |
|---|---|---|---|
toleranceMs | number | 300000 (5 min) | Maximum age in milliseconds for a webhook timestamp to be considered valid. Requests older than this are rejected as potential replay attacks |
// Default: 5-minute tolerance
const receiver = new WebhookReceiver('whsec_my_secret');
// Custom: 30-second tolerance for stricter security
const strict = new WebhookReceiver('whsec_my_secret', { toleranceMs: 30_000 });verify(body, headers)
Verify a webhook request and return the parsed event.
verify(
body: string,
headers: Record<string, string | string[] | undefined>
): WebhookEvent| Parameter | Type | Required | Description |
|---|---|---|---|
body | string | Yes | The raw request body string. Must be the exact bytes received — do not re-serialize or parse before verification |
headers | Record<string, string | string[] | undefined> | Yes | HTTP headers object. In Express this is req.headers. The receiver looks for X-Webhook-Signature (case-insensitive) |
Returns: WebhookEvent
interface WebhookEvent {
/** The verified and parsed RealtimeEvent */
event: RealtimeEvent;
/** Timestamp (ms) from the signature header */
timestamp: number;
/** The raw body string that was verified */
rawBody: string;
}Throws: WebhookVerificationError if verification fails (see Error Handling below).
const { event, timestamp, rawBody } = receiver.verify(req.body, req.headers);
console.log(event.id); // "evt_a1b2c3d4"
console.log(event.topic); // "session.status"
console.log(event.source); // "database"
console.log(event.type); // "row.update"
console.log(event.payload); // { sessionId: "abc123", status: "active" }verifyWebhook() (Standalone Function)
A convenience function for one-off verification without instantiating a class:
import { verifyWebhook } from '@smarterservices/realtime';
const { event } = verifyWebhook(body, headers, 'whsec_my_secret');verifyWebhook(
body: string,
headers: Record<string, string | string[] | undefined>,
secret: string,
options?: WebhookReceiverOptions
): WebhookEventError Handling
All verification failures throw a WebhookVerificationError with a machine-readable code property:
import { WebhookVerificationError } from '@smarterservices/realtime';
try {
const { event } = receiver.verify(body, headers);
// Process event...
} catch (err) {
if (err instanceof WebhookVerificationError) {
console.error(`Webhook rejected [${err.code}]: ${err.message}`);
}
}| Code | Meaning |
|---|---|
MISSING_SIGNATURE | The X-Webhook-Signature header was not found in the request |
INVALID_SIGNATURE_FORMAT | The header is not in the expected t=<timestamp>,v1=<hex> format |
TIMESTAMP_EXPIRED | The signature timestamp is outside the tolerance window (replay protection) |
SIGNATURE_MISMATCH | The computed HMAC does not match — the secret is wrong or the body was tampered with |
INVALID_PAYLOAD | The body is not valid JSON or is missing required RealtimeEvent fields (id, topic) |
Signature Format
The platform signs every webhook delivery with HMAC-SHA256. The signature is sent in the X-Webhook-Signature header using this format:
t=<timestamp>,v1=<hex-encoded HMAC-SHA256>The signed content is ${timestamp}.${rawBody} — the timestamp concatenated with a dot and the raw JSON body. Including the timestamp in the signed content prevents replay attacks even if an attacker captures a valid signature.
Headers sent with each delivery:
| Header | Description |
|---|---|
X-Webhook-Signature | Signature in t=<ts>,v1=<sig> format |
X-Webhook-Timestamp | Signing timestamp (milliseconds) |
Content-Type | application/json |
Framework Examples
Express
import express from 'express';
import { WebhookReceiver } from '@smarterservices/realtime';
const app = express();
const receiver = new WebhookReceiver(process.env.WEBHOOK_SECRET!);
// IMPORTANT: Use raw/text body parsing — not JSON parsing —
// so the signature can be verified against the original bytes.
app.post(
'/webhooks/realtime',
express.text({ type: 'application/json' }),
(req, res) => {
try {
const { event } = receiver.verify(req.body, req.headers);
switch (event.topic) {
case 'session.status':
handleSessionUpdate(event.payload);
break;
case 'order.created':
handleNewOrder(event.payload);
break;
default:
console.log('Unhandled topic:', event.topic);
}
res.sendStatus(200);
} catch (err) {
console.error('Webhook error:', err);
res.sendStatus(400);
}
}
);Fastify
import Fastify from 'fastify';
import { WebhookReceiver } from '@smarterservices/realtime';
const fastify = Fastify();
const receiver = new WebhookReceiver(process.env.WEBHOOK_SECRET!);
// Fastify provides the raw body via request.body when using
// content-type-parser for text
fastify.addContentTypeParser(
'application/json',
{ parseAs: 'string' },
(req, body, done) => done(null, body)
);
fastify.post('/webhooks/realtime', async (request, reply) => {
try {
const { event } = receiver.verify(
request.body as string,
request.headers as Record<string, string>
);
console.log('Event:', event.topic, event.payload);
return reply.code(200).send({ ok: true });
} catch {
return reply.code(400).send({ error: 'Invalid webhook' });
}
});Next.js API Route (App Router)
import { NextRequest, NextResponse } from 'next/server';
import { WebhookReceiver, WebhookVerificationError } from '@smarterservices/realtime';
const receiver = new WebhookReceiver(process.env.WEBHOOK_SECRET!);
export async function POST(req: NextRequest) {
const body = await req.text();
const headers: Record<string, string> = {};
req.headers.forEach((value, key) => { headers[key] = value; });
try {
const { event } = receiver.verify(body, headers);
console.log('Webhook event:', event.topic, event.type);
return NextResponse.json({ received: true });
} catch (err) {
if (err instanceof WebhookVerificationError) {
return NextResponse.json({ error: err.code }, { status: 400 });
}
return NextResponse.json({ error: 'Internal error' }, { status: 500 });
}
}Important: Raw Body Requirement
The webhook signature is computed over the exact raw bytes of the request body. If your framework parses the body as JSON before your handler runs, you'll need to ensure the raw string is also available.
Most frameworks provide a way to access the raw body:
| Framework | Approach |
|---|---|
| Express | Use express.text({ type: 'application/json' }) middleware on the webhook route |
| Fastify | Register a custom content-type parser with parseAs: 'string' |
| Next.js | Use await req.text() in the App Router |
| Koa | Use koa-body with { text: true } or ctx.request.rawBody |
TypeScript Types
All types are exported from the SDK:
import type { WebhookReceiverOptions, WebhookEvent } from '@smarterservices/realtime';
import { WebhookReceiver, verifyWebhook, WebhookVerificationError } from '@smarterservices/realtime';