Skip to content

Webhooks

Slotty Labs sends webhook events to your server when important actions occur. This guide covers all event types, payload formats, signing, and retry policies.

Event Types

All 16 Event Types

CategoryEvent TypeDescription
Gameround.completedA game round finished (win or loss)
Gameround.cancelledA round was cancelled (e.g., server error)
Gameround.jackpot_wonA jackpot was hit
Financialwallet.bet_placedA bet was deducted from player balance
Financialwallet.win_creditedA win was credited to player balance
Financialwallet.rollbackA transaction was rolled back
Financialwallet.deposit_confirmedA crypto deposit was confirmed on-chain
Financialwallet.withdrawal_completedA withdrawal was processed and confirmed
Financialwallet.withdrawal_failedA withdrawal failed
Playerplayer.createdA new player account was created
Playerplayer.suspendedA player was suspended
Playerplayer.self_excludedA player self-excluded
Playerplayer.limit_reachedA player hit a responsible gaming limit
Systemsystem.maintenanceScheduled maintenance notification
Systemsystem.incidentUnplanned incident notification
Systemsystem.game_version_updatedA game was updated to a new version

Default Subscriptions

These 5 events are automatically enabled for all operators:

  • round.completed
  • wallet.deposit_confirmed
  • wallet.withdrawal_completed
  • player.limit_reached
  • system.maintenance

Optional Subscriptions

These 11 events must be opted in via the operator dashboard or API:

  • wallet.bet_placed
  • wallet.win_credited
  • wallet.rollback
  • wallet.withdrawal_failed
  • player.created
  • player.suspended
  • player.self_excluded
  • round.cancelled
  • round.jackpot_won
  • system.incident
  • system.game_version_updated

Payload Envelope

All webhook payloads follow this structure:

typescript
interface WebhookEnvelope<T = unknown> {
  id: string;              // Unique event ID (for idempotency)
  type: string;            // Event type, e.g. "round.completed"
  apiVersion: string;      // API version, e.g. "2025-01-01"
  tenantId: string;        // Your operator tenant ID
  timestamp: string;       // ISO 8601 when event occurred
  deliveredAt: string;     // ISO 8601 when webhook was sent
  data: T;                 // Event-specific payload
  meta: {
    attempt: number;       // Delivery attempt number (1-based)
    environment: 'sandbox' | 'production';
  };
}

Event Payload Examples

round.completed

typescript
interface RoundCompletedData {
  roundId: string;
  gameId: string;
  playerId: string;
  externalPlayerId: string;
  betAmount: number;
  winAmount: number;
  currency: string;
  rtp: number;
  duration: number;          // milliseconds
  provablyFair: {
    serverSeedHash: string;
    clientSeed: string;
    nonce: number;
  };
}

wallet.deposit_confirmed

typescript
interface DepositConfirmedData {
  depositId: string;
  playerId: string;
  externalPlayerId: string;
  amount: string;            // Decimal string for precision
  currency: string;          // e.g. "USDT"
  chain: string;             // e.g. "ethereum"
  txHash: string;            // On-chain transaction hash
  confirmations: number;
  fromAddress: string;
  toAddress: string;         // Player's deposit address
  creditedBalance: number;   // New player balance
}

player.limit_reached

typescript
interface LimitReachedData {
  playerId: string;
  externalPlayerId: string;
  limitType: 'deposit' | 'loss' | 'session' | 'wagering';
  limitValue: number;
  currentValue: number;
  period: 'daily' | 'weekly' | 'monthly';
  action: 'warned' | 'blocked';
}

Webhook Signing

Every webhook request includes a signature for verification:

How Signing Works

payload = "${timestamp}.${body}"
signature = HMAC-SHA256(webhookSecret, payload)

Request Headers

HeaderDescription
X-Slotty-Signaturesha256=<hex-encoded HMAC>
X-Slotty-TimestampUnix timestamp (seconds) of when the webhook was sent
Content-Typeapplication/json

Verification with SDK

typescript
import { WebhookVerifier } from '@slottylabs/sdk';

const verifier = new WebhookVerifier('whsec_xyz789...');

app.post('/webhooks/slotty', (req, res) => {
  try {
    const event = verifier.constructEvent(
      req.rawBody,                           // Raw request body string
      req.headers['x-slotty-signature'],     // Signature header
      req.headers['x-slotty-timestamp'],     // Timestamp header
    );

    console.log(`Received event: ${event.type}`, event.data);
    res.status(200).json({ received: true });
  } catch (err) {
    console.error('Webhook verification failed:', err.message);
    res.status(400).json({ error: 'Invalid signature' });
  }
});

Manual Verification

typescript
import crypto from 'crypto';

function verifyWebhook(
  rawBody: string,
  signature: string,
  timestamp: string,
  secret: string,
): boolean {
  // Check timestamp tolerance (±5 minutes)
  const now = Math.floor(Date.now() / 1000);
  const ts = parseInt(timestamp, 10);
  if (Math.abs(now - ts) > 300) return false;

  // Compute expected signature
  const payload = `${timestamp}.${rawBody}`;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  const actual = signature.replace('sha256=', '');

  // Timing-safe comparison
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(actual),
  );
}

Retry Policy

If your endpoint doesn't respond with a 2xx status within 10 seconds, Slotty Labs will retry:

AttemptDelay After Previous
1Immediate
230 seconds
32 minutes
410 minutes
51 hour
64 hours
712 hours
824 hours

After 8 failed attempts, the event is moved to a dead-letter queue. You can replay dead-lettered events from the operator dashboard.

Idempotency

At-Least-Once Delivery

Webhooks are delivered with at-least-once semantics. Your endpoint may receive the same event more than once, especially during retries.

Always use the event id field to deduplicate events on your end.

typescript
// Example: Redis-based idempotency check
const processed = await redis.get(`webhook:processed:${event.id}`);
if (processed) {
  return res.status(200).json({ received: true }); // Already handled
}

// Process the event...
await handleEvent(event);

// Mark as processed (TTL: 7 days)
await redis.set(`webhook:processed:${event.id}`, '1', 'EX', 604800);