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.
| Category | Event Type | Description |
|---|---|---|
| Game | round.completed | A game round finished (win or loss). High volume — one event per round. |
| Game | round.cancelled | A round was rolled back and the bet refunded (e.g., server error) |
| Game | round.jackpot_won | A player hit your progressive jackpot — see Progressive Jackpots |
| Wallet | wallet.deposit_confirmed | A crypto or fiat deposit was credited to a player |
| Wallet | wallet.withdrawal_completed | A withdrawal was approved and funds sent |
| Wallet | wallet.withdrawal_failed | A withdrawal was rejected and the funds returned to the player |
| Player | player.created | A 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:
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
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.
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
| Header | Description |
|---|---|
X-Slotty-Signature | sha256=<hex-encoded HMAC> |
X-Slotty-Timestamp | Unix timestamp (seconds) of when the webhook was sent |
Content-Type | application/json |
Verification with SDK
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
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:
| Attempt | Delay After Previous |
|---|---|
| 1 | Immediate |
| 2 | 30 seconds |
| 3 | 2 minutes |
| 4 | 10 minutes |
| 5 | 1 hour |
| 6 | 4 hours |
| 7 | 12 hours |
| 8 | 24 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.
// 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);