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 ​

These are the events the platform emits today. Subscribe to them on your dashboard under Settings → Webhooks — the panel lists exactly this set, and the API rejects unknown event names.

CategoryEvent TypeDescription
Gameround.completedA game round finished (win or loss). High volume — one event per round.
Gameround.cancelledA round was rolled back and the bet refunded (e.g., server error)
Gameround.jackpot_wonA player hit your progressive jackpot — see Progressive Jackpots
Walletwallet.deposit_confirmedA crypto or fiat deposit was credited to a player
Walletwallet.withdrawal_completedA withdrawal was approved and funds sent
Walletwallet.withdrawal_failedA withdrawal was rejected and the funds returned to the player
Playerplayer.createdA new player launched a game for the first time

Every payload carries your external playerId (plus our internal playerUuid), and money as both a decimal string (amount) and native minor units (amountMinor). Crypto wallet events include chain and txHash when available.

Catalog grows with the platform

Additional event types (responsible-gaming, system notices) will appear in the dashboard as they go live — anything listed there is guaranteed to be emitted end-to-end.

Payload Envelope ​

All webhook payloads follow this structure:

typescript
interface WebhookPayload<T = Record<string, unknown>> {
  id: string;              // Unique event ID (UUID v7, for idempotency)
  type: string;            // Event type, e.g. "round.completed"
  apiVersion: 'v1';        // API version that produced this event
  tenantId: string;        // Your operator tenant ID
  timestamp: string;       // ISO 8601 when event occurred
  deliveredAt: string;     // ISO 8601 when this delivery attempt was made
  data: T;                 // Event-specific payload
  meta: {
    attemptNumber: number; // Delivery attempt number (1-based)
    maxAttempts: number;   // Total attempts before dead-letter
    webhookId: string;     // Your webhook endpoint configuration ID
  };
}

Event Payload Examples ​

Money is always strings

All monetary amounts are strings of integer minor units (e.g. "1000" = €10.00) — never floating-point numbers.

round.completed ​

typescript
interface RoundCompletedEventData {
  roundId: string;
  playerId: string;         // Your external player ID (as passed at SSO time)
  gameId: string;
  gameType: 'single_step' | 'multi_step' | 'session_based';
  currency: string;
  totalBet: string;          // minor units
  totalWin: string;          // minor units
  netResult: string;         // minor units (totalWin - totalBet)
  actionCount: number;
  endReason: 'completed' | 'cash_out' | 'timeout' | 'hammer_break' | 'fbi_caught' | 'killed';
  balanceAfter: string;      // minor units
  paytableVariant: string;
  provablyFair: {
    serverSeedHash: string;
    clientSeed: string;
    nonce: number;
    verificationUrl: string;
  } | null;
  startedAt: string;         // ISO 8601
  completedAt: string;       // ISO 8601
}

round.jackpot_won ​

Sent the moment a progressive jackpot award is committed to the ledger (never for a rolled-back round). See Progressive Jackpots for how pools work.

typescript
interface RoundJackpotWonEventData {
  poolId: string;
  poolName: string;          // e.g. "USDT Progressive Jackpot"
  cycleNumber: number;       // which cycle of this pool was won
  currency: string;
  amount: string;            // whole units, e.g. "125.500000"
  amountMinor: string;       // minor units, e.g. "125500000"
  playerId: string;          // internal player UUID
  playerExternalId: string | null; // YOUR player id (as passed at SSO time)
  gameId: string;            // internal game UUID
  gameSlug: string;          // e.g. "fox"
  roundId: string | null;
  transactionId: string;     // ledger transaction of the payout
  lockedToBonus: boolean;    // true if the win went to the player's bonus
                             // balance (they had an unconverted bonus)
}

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);