WebhookVerifier Reference
The WebhookVerifier class provides secure verification of incoming webhook payloads from Slotty Labs.
Constructor
typescript
import { WebhookVerifier } from '@slottylabs/sdk';
const verifier = new WebhookVerifier(
secret: string,
toleranceSeconds?: number,
);| Parameter | Type | Default | Description |
|---|---|---|---|
secret | string | — | Your webhook secret (whsec_* prefix) |
toleranceSeconds | number | 300 | Timestamp tolerance in seconds (±5 minutes) |
Methods
constructEvent()
Verifies the webhook signature and returns a typed event object. Throws WebhookVerificationError on failure.
typescript
const event = verifier.constructEvent(
rawBody: string,
signature: string,
timestamp: string,
): VerifiedEvent;| Parameter | Type | Description |
|---|---|---|
rawBody | string | The raw request body as a string (not parsed JSON) |
signature | string | Value of the X-Slotty-Signature header |
timestamp | string | Value of the X-Slotty-Timestamp header |
Returns: VerifiedEvent — the verified and parsed webhook event.
Throws: WebhookVerificationError if:
- The signature does not match
- The timestamp is outside the tolerance window
- The body cannot be parsed as JSON
verify()
Verifies the webhook signature and returns a boolean. Does not throw.
typescript
const isValid = verifier.verify(
rawBody: string,
signature: string,
timestamp: string,
): boolean;| Parameter | Type | Description |
|---|---|---|
rawBody | string | The raw request body as a string |
signature | string | Value of the X-Slotty-Signature header |
timestamp | string | Value of the X-Slotty-Timestamp header |
Returns: true if the signature is valid and the timestamp is within tolerance, false otherwise.
WebhookVerificationError
typescript
class WebhookVerificationError extends Error {
/** Error reason code */
reason: 'invalid_signature' | 'timestamp_expired' | 'invalid_payload';
/** Human-readable error message */
message: string;
}VerifiedEvent
typescript
interface VerifiedEvent {
/** Unique event ID (for idempotency) */
id: string;
/** Event type (e.g. "round.completed") */
type: string;
/** API version (e.g. "2025-01-01") */
apiVersion: string;
/** Operator tenant ID */
tenantId: string;
/** When the event occurred (ISO 8601) */
timestamp: string;
/** When the webhook was sent (ISO 8601) */
deliveredAt: string;
/** Event-specific payload */
data: Record<string, unknown>;
/** Delivery metadata */
meta: {
attempt: number;
environment: 'sandbox' | 'production';
};
}Usage Examples
Express.js
typescript
import express from 'express';
import { WebhookVerifier, WebhookVerificationError } from '@slottylabs/sdk';
const app = express();
const verifier = new WebhookVerifier(process.env.SLOTTY_WEBHOOK_SECRET!);
// IMPORTANT: Use raw body parser for webhook routes
app.post(
'/webhooks/slotty',
express.raw({ type: 'application/json' }),
(req, res) => {
try {
const event = verifier.constructEvent(
req.body.toString(),
req.headers['x-slotty-signature'] as string,
req.headers['x-slotty-timestamp'] as string,
);
switch (event.type) {
case 'round.completed':
handleRoundCompleted(event.data);
break;
case 'wallet.deposit_confirmed':
handleDeposit(event.data);
break;
case 'player.limit_reached':
handleLimitReached(event.data);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
res.status(200).json({ received: true });
} catch (err) {
if (err instanceof WebhookVerificationError) {
console.error(`Verification failed: ${err.reason} — ${err.message}`);
res.status(400).json({ error: err.reason });
} else {
console.error('Unexpected error:', err);
res.status(500).json({ error: 'Internal error' });
}
}
},
);
app.listen(3000);Fastify
typescript
import Fastify from 'fastify';
import { WebhookVerifier, WebhookVerificationError } from '@slottylabs/sdk';
const fastify = Fastify({ logger: true });
const verifier = new WebhookVerifier(process.env.SLOTTY_WEBHOOK_SECRET!);
// Register raw body parsing
fastify.addContentTypeParser(
'application/json',
{ parseAs: 'string' },
(req, body, done) => {
done(null, body);
},
);
fastify.post('/webhooks/slotty', async (request, reply) => {
try {
const event = verifier.constructEvent(
request.body as string,
request.headers['x-slotty-signature'] as string,
request.headers['x-slotty-timestamp'] as string,
);
switch (event.type) {
case 'round.completed':
await handleRoundCompleted(event.data);
break;
case 'wallet.deposit_confirmed':
await handleDeposit(event.data);
break;
case 'player.limit_reached':
await handleLimitReached(event.data);
break;
default:
fastify.log.info(`Unhandled event: ${event.type}`);
}
return { received: true };
} catch (err) {
if (err instanceof WebhookVerificationError) {
reply.status(400);
return { error: err.reason };
}
throw err;
}
});
fastify.listen({ port: 3000 });Raw Body Required
You must use the raw (unparsed) request body for signature verification. If your framework automatically parses JSON, the re-serialized body may differ from the original, causing verification failures.