Appearance
Quick Start
This guide takes you from "I have an API key" to "I executed my first spin" in under 10 minutes.
Prerequisites
- Node.js 20+ installed (download)
- A KINGSTONE sandbox API key (provided by Predigy — all keys start with
ks_) - The sandbox base URL (provided by Predigy, e.g.,
https://sandbox.kingstone.dev/api)
Step 1: Install the SDK
bash
npm install @kingstoneapp/sdkThe SDK has zero runtime dependencies — it uses the built-in fetch API.
Step 2: Initialize the Client
Create a file called kingstone-test.ts:
ts
import { KingstoneClient } from '@kingstoneapp/sdk';
const client = new KingstoneClient({
apiKey: process.env.KINGSTONE_API_KEY!,
baseUrl: process.env.KINGSTONE_BASE_URL,
sandbox: true,
});Set your environment variables:
bash
export KINGSTONE_API_KEY=ks_sandbox_your_key_here
export KINGSTONE_BASE_URL=http://localhost:3001/api # or your sandbox URLStep 3: Upload a PAR Sheet
A PAR sheet (Probability Accounting Report) defines the math model for a game — symbols, prize tiers, probabilities, and payouts. KINGSTONE uses this to pre-compute all outcomes.
Here is a minimal 3-reel PAR sheet you can use for testing:
ts
const parSheet = {
gameTitle: 'Sandbox Classic 3-Reel',
manufacturer: 'Partner Test Studio',
numReels: 3,
virtualStopsPerReel: [32, 32, 32],
certifiedRtp: 0.925,
denominationUsd: 0.25,
symbols: [
{ name: 'Seven', display: '7' },
{ name: 'Bar', display: 'BAR' },
{ name: 'Cherry', display: 'CH' },
{ name: 'Plum', display: 'PL' },
{ name: 'Blank', display: 'BL' },
],
tiers: [
{ tierId: 0, name: 'No Win', probability: 0.7, payMultiplier: 0 },
{ tierId: 1, name: 'Small Win', probability: 0.15, payMultiplier: 2 },
{ tierId: 2, name: 'Medium Win', probability: 0.08, payMultiplier: 3 },
{ tierId: 3, name: 'Large Win', probability: 0.04, payMultiplier: 4 },
{ tierId: 4, name: 'Big Win', probability: 0.02, payMultiplier: 5 },
{ tierId: 5, name: 'Huge Win', probability: 0.008, payMultiplier: 6 },
{ tierId: 6, name: 'Jackpot', probability: 0.002, payMultiplier: 38.5 },
],
combinations: [
{ reelSymbols: ['Blank', 'Blank', 'Blank'], comboString: 'BL|BL|BL', payMultiplier: 0, comboProbability: 0.7, tierId: 0, displayWeight: 1 },
{ reelSymbols: ['Cherry', 'Cherry', 'Blank'], comboString: 'CH|CH|BL', payMultiplier: 2, comboProbability: 0.075, tierId: 1, displayWeight: 1 },
{ reelSymbols: ['Plum', 'Plum', 'Plum'], comboString: 'PL|PL|PL', payMultiplier: 2, comboProbability: 0.075, tierId: 1, displayWeight: 1 },
{ reelSymbols: ['Cherry', 'Cherry', 'Cherry'], comboString: 'CH|CH|CH', payMultiplier: 3, comboProbability: 0.08, tierId: 2, displayWeight: 1 },
{ reelSymbols: ['Bar', 'Bar', 'Bar'], comboString: 'BAR|BAR|BAR', payMultiplier: 4, comboProbability: 0.04, tierId: 3, displayWeight: 1 },
{ reelSymbols: ['Bar', 'Seven', 'Bar'], comboString: 'BAR|7|BAR', payMultiplier: 5, comboProbability: 0.02, tierId: 4, displayWeight: 1 },
{ reelSymbols: ['Seven', 'Seven', 'Bar'], comboString: '7|7|BAR', payMultiplier: 6, comboProbability: 0.008, tierId: 5, displayWeight: 1 },
{ reelSymbols: ['Seven', 'Seven', 'Seven'], comboString: '7|7|7', payMultiplier: 38.5, comboProbability: 0.002, tierId: 6, displayWeight: 1 },
],
};
const uploaded = await client.uploadParSheet(parSheet);
console.log(`PAR sheet created: ID ${uploaded.parSheetId}`);Math Requirements
- All
probabilityvalues must sum to exactly 1.0 sum(probability * payMultiplier)must equalcertifiedRtp(e.g., 0.925)- KINGSTONE validates the math on upload and rejects invalid data
Step 4: Register a Game
Link the PAR sheet to a game configuration:
ts
const game = await client.registerGame({
parSheetId: uploaded.parSheetId,
displayName: 'My Test Slot',
configName: 'TEST_SLOT_001',
});
console.log(`Game registered: ID ${game.gameConfigId}`);Fixed-wager model
Every KINGSTONE game is fixed-wager: each spin wagers exactly the PAR sheet's denominationUsd (here, $0.25). You do not set a wager band — omit minWagerUsd / maxWagerUsd / wagerIncrementUsd. If you do pass them, each must equal the denomination, or the call is rejected with KS-4103.
Step 5: Execute a Spin
This is the core operation. Send a spin request with the game ID, your player's ID, and the wager amount:
ts
const result = await client.spin({
gameId: game.gameConfigId,
playerId: 'player-001', // Your external player ID
wagerUsd: 0.25, // Must equal the game's fixed wager (its PAR denomination, $0.25)
});
console.log('--- Spin Result ---');
console.log(`Spin ID: ${result.spinId}`);
console.log(`Prize Tier: ${result.outcome.prizeTier}`);
console.log(`Payout: $${result.outcome.payoutUsd.toFixed(2)}`);
// `presentation` is a discriminated union — narrow on `type` to read its fields.
const reveal = result.presentation.type === 'slot'
? result.presentation.comboString // slot: the symbol combination
: result.presentation.prizeName; // raffle: the prize name
console.log(`Reveal: ${reveal}`);
console.log(`Balance: $${result.balance.balanceUsd.toFixed(2)}`);What happens behind the scenes:
- KINGSTONE resolves your player ID (auto-creates on first spin)
- Deducts the wager from the player's credit balance
- Retrieves the next pre-computed outcome from the sealed queue
- Selects a visual presentation matching the outcome tier
- Credits winnings (if any) and returns the full result
The player's balance starts with sandbox test credits. In production, you manage deposits and withdrawals through your own payment system.
Step 6: Check a Settlement Report
Settlement reports are generated daily at 02:00 UTC. After running some spins, you can fetch them:
ts
const { reports } = await client.listSettlements({ page: 1, limit: 5 });
for (const report of reports) {
console.log(`${report.reportDate}: ${report.totalSpins} spins, GGR $${report.ggrUsd.toFixed(2)}`);
}Step 7: Set Up a Webhook (Optional)
Receive real-time notifications for large wins, settlement reports, and more:
ts
const webhook = await client.registerWebhook({
url: 'https://your-server.com/webhooks/kingstone',
events: ['spin.large_win', 'settlement.report_ready'],
});
// IMPORTANT: Save the secret — it is shown only once
console.log(`Webhook ID: ${webhook.id}`);
console.log(`Secret: ${webhook.secret}`);See the Webhooks Guide for HMAC signature verification.
Complete Working Example
Here is the full script combining all the steps above:
ts
import { KingstoneClient, KingstoneApiError } from '@kingstoneapp/sdk';
async function main() {
// Initialize
const client = new KingstoneClient({
apiKey: process.env.KINGSTONE_API_KEY!,
baseUrl: process.env.KINGSTONE_BASE_URL,
sandbox: true,
});
try {
// Check if we already have games registered
const { games } = await client.listGames();
let gameId: number;
if (games.length > 0) {
gameId = games[0].gameConfigId;
console.log(`Using existing game: ${games[0].displayName} (ID: ${gameId})`);
} else {
// Upload PAR sheet + register game (see Steps 3-4 above)
const uploaded = await client.uploadParSheet({
gameTitle: 'Sandbox Classic 3-Reel',
manufacturer: 'Partner Test Studio',
numReels: 3,
virtualStopsPerReel: [32, 32, 32],
certifiedRtp: 0.925,
denominationUsd: 0.25,
symbols: [
{ name: 'Seven', display: '7' },
{ name: 'Bar', display: 'BAR' },
{ name: 'Cherry', display: 'CH' },
{ name: 'Plum', display: 'PL' },
{ name: 'Blank', display: 'BL' },
],
tiers: [
{ tierId: 0, name: 'No Win', probability: 0.7, payMultiplier: 0 },
{ tierId: 1, name: 'Small Win', probability: 0.15, payMultiplier: 2 },
{ tierId: 2, name: 'Medium Win', probability: 0.08, payMultiplier: 3 },
{ tierId: 3, name: 'Large Win', probability: 0.04, payMultiplier: 4 },
{ tierId: 4, name: 'Big Win', probability: 0.02, payMultiplier: 5 },
{ tierId: 5, name: 'Huge Win', probability: 0.008, payMultiplier: 6 },
{ tierId: 6, name: 'Jackpot', probability: 0.002, payMultiplier: 38.5 },
],
combinations: [
{ reelSymbols: ['Blank','Blank','Blank'], comboString: 'BL|BL|BL', payMultiplier: 0, comboProbability: 0.7, tierId: 0, displayWeight: 1 },
{ reelSymbols: ['Cherry','Cherry','Blank'], comboString: 'CH|CH|BL', payMultiplier: 2, comboProbability: 0.075, tierId: 1, displayWeight: 1 },
{ reelSymbols: ['Plum','Plum','Plum'], comboString: 'PL|PL|PL', payMultiplier: 2, comboProbability: 0.075, tierId: 1, displayWeight: 1 },
{ reelSymbols: ['Cherry','Cherry','Cherry'], comboString: 'CH|CH|CH', payMultiplier: 3, comboProbability: 0.08, tierId: 2, displayWeight: 1 },
{ reelSymbols: ['Bar','Bar','Bar'], comboString: 'BAR|BAR|BAR', payMultiplier: 4, comboProbability: 0.04, tierId: 3, displayWeight: 1 },
{ reelSymbols: ['Bar','Seven','Bar'], comboString: 'BAR|7|BAR', payMultiplier: 5, comboProbability: 0.02, tierId: 4, displayWeight: 1 },
{ reelSymbols: ['Seven','Seven','Bar'], comboString: '7|7|BAR', payMultiplier: 6, comboProbability: 0.008, tierId: 5, displayWeight: 1 },
{ reelSymbols: ['Seven','Seven','Seven'], comboString: '7|7|7', payMultiplier: 38.5, comboProbability: 0.002, tierId: 6, displayWeight: 1 },
],
});
const game = await client.registerGame({
parSheetId: uploaded.parSheetId,
displayName: 'My Test Slot',
configName: 'TEST_SLOT_001',
});
gameId = game.gameConfigId;
console.log(`Created game: ${game.displayName} (ID: ${gameId})`);
}
// Execute 5 spins
for (let i = 1; i <= 5; i++) {
const result = await client.spin({
gameId,
playerId: 'quickstart-player',
wagerUsd: 0.25,
});
const won = result.outcome.payoutUsd > 0 ? `WIN $${result.outcome.payoutUsd.toFixed(2)}` : 'No win';
const reveal = result.presentation.type === 'slot' ? result.presentation.comboString : result.presentation.prizeName;
console.log(`Spin ${i}: ${reveal} — ${won} (Balance: $${result.balance.balanceUsd.toFixed(2)})`);
}
} catch (error) {
if (error instanceof KingstoneApiError) {
console.error(`API Error [${error.errorCode}]: ${error.message} (HTTP ${error.statusCode})`);
} else {
throw error;
}
}
}
main();Run it:
bash
KINGSTONE_API_KEY=ks_sandbox_xxx KINGSTONE_BASE_URL=http://localhost:3001/api npx tsx kingstone-test.tsTroubleshooting
"Missing X-API-Key header" (KS-4001)
You forgot to include the API key. Make sure KINGSTONE_API_KEY is set in your environment, or pass it directly to the constructor.
"Invalid API key" (KS-4002)
The key does not match any record. Double-check:
- The key was copied correctly (no trailing spaces)
- You are using a sandbox key (
ks_) against the sandbox URL - The key has not been revoked
"Wager must equal the game's fixed wager" (KS-4103)
Every game is fixed-wager: wagerUsd must equal the game's wager, which is its PAR sheet's denominationUsd. Sending any other amount is rejected. Check the game's fixed wager (all three fields are equal to it):
ts
const { games } = await client.listGames();
console.log('Fixed wager:', games[0].minWagerUsd); // === maxWagerUsd === wagerIncrementUsd === denomination"Invalid PAR sheet data" (KS-4104)
The PAR sheet math does not validate. Common causes:
- Probabilities do not sum to exactly 1.0 (check for floating-point rounding)
sum(probability * payMultiplier)does not equalcertifiedRtp- A combination references a symbol name not in the
symbolsarray - A combination references a
tierIdnot in thetiersarray
"Rate limit exceeded" (KS-4291 / KS-4292)
You are sending too many requests. Check the X-RateLimit-Remaining and Retry-After response headers. Default limits:
- Spins: 300/minute
- Other API calls: 600/minute
Wait for the Retry-After period, then retry. See Rate Limits for details.
"Game outcome queue exhausted" (KS-4203)
All pre-computed outcomes have been consumed and new blocks have not been generated yet. This is rare — KINGSTONE auto-generates new blocks when reserves drop below 2. In sandbox, blocks are only 100 entries, so this can happen with rapid testing. Wait a minute and retry.
Next Steps
- Executing Spins — detailed spin response format and frontend integration
- Settlements — daily reconciliation workflow
- Webhooks — real-time event notifications
- Error Handling — retry logic and error code reference
