Skip to content

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,
);
ParameterTypeDefaultDescription
secretstringYour webhook secret (whsec_* prefix)
toleranceSecondsnumber300Timestamp 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;
ParameterTypeDescription
rawBodystringThe raw request body as a string (not parsed JSON)
signaturestringValue of the X-Slotty-Signature header
timestampstringValue 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;
ParameterTypeDescription
rawBodystringThe raw request body as a string
signaturestringValue of the X-Slotty-Signature header
timestampstringValue 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.