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
| Category | Event Type | Description |
|---|---|---|
| Game | round.completed | A game round finished (win or loss) |
| Game | round.cancelled | A round was cancelled (e.g., server error) |
| Game | round.jackpot_won | A jackpot was hit |
| Financial | wallet.bet_placed | A bet was deducted from player balance |
| Financial | wallet.win_credited | A win was credited to player balance |
| Financial | wallet.rollback | A transaction was rolled back |
| Financial | wallet.deposit_confirmed | A crypto deposit was confirmed on-chain |
| Financial | wallet.withdrawal_completed | A withdrawal was processed and confirmed |
| Financial | wallet.withdrawal_failed | A withdrawal failed |
| Player | player.created | A new player account was created |
| Player | player.suspended | A player was suspended |
| Player | player.self_excluded | A player self-excluded |
| Player | player.limit_reached | A player hit a responsible gaming limit |
| System | system.maintenance | Scheduled maintenance notification |
| System | system.incident | Unplanned incident notification |
| System | system.game_version_updated | A game was updated to a new version |
Default Subscriptions
These 5 events are automatically enabled for all operators:
round.completedwallet.deposit_confirmedwallet.withdrawal_completedplayer.limit_reachedsystem.maintenance
Optional Subscriptions
These 11 events must be opted in via the operator dashboard or API:
wallet.bet_placedwallet.win_creditedwallet.rollbackwallet.withdrawal_failedplayer.createdplayer.suspendedplayer.self_excludedround.cancelledround.jackpot_wonsystem.incidentsystem.game_version_updated
Payload Envelope
All webhook payloads follow this structure:
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
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
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
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
| 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);