Documentation
npm SDK · Godot 4 plugin · REST API reference · webhooks.
lightleaderboard npm package
TypeScript-first SDK for Node.js 18+, browsers, Bun, Deno, and Cloudflare Workers.
Zero dependencies — uses native fetch and the Web Crypto API.
Keep your apiKey server-side; the SDK is safe to bundle for Node.js backends or server functions.
Install
npm install lightleaderboard
# or
yarn add lightleaderboard
# or
pnpm add lightleaderboardRequires Node.js 18+ or any runtime with native fetch + Web Crypto API (Bun, Deno, Cloudflare Workers, modern browsers).
Initialize the client
Create a single instance per game and reuse it across your server. Find your API key and game ID in the developer portal → your game → API Keys.
import { LightLeaderboard } from 'lightleaderboard';
const lb = new LightLeaderboard({
apiKey: process.env.LLB_API_KEY, // from the LightLeaderboard dashboard
gameId: 'my_game', // your game's reference ID
});apiKey to browser clients.
For browser-based games (Kaboom.js, Phaser, PixiJS), call a server endpoint that uses the SDK — see the Express example below.Submit a score
Submit a score and get back the player's rank, personal-best flag, and total player count in a single call — no extra request needed.
const result = await lb.submitScore({
score: 9500,
playerRefId: 'player-123', // your internal player ID
playerName: 'Alice', // display name
submissionId: crypto.randomUUID(), // idempotency key — prevents duplicate submissions
playTimeMs: 120_000, // optional: run duration in ms
seasonId: 'season-3', // optional: season bucket
teamId: 'team-red', // optional: team bucket
metadata: { level: 5, combo: 12 }, // optional: arbitrary JSON
});
// Result comes back in the same call — no second request needed
console.log(result.rank); // 4 ← player's current rank
console.log(result.isPersonalBest); // true
console.log(result.totalPlayers); // 1024
console.log(result.deduped); // true if submissionId was already seenscorenumber✓The player's scoreplayerRefIdstringYour internal player ID — enables deduplication and rank trackingplayerNamestringDisplay name shown on the leaderboardsubmissionIdstringIdempotency key — resubmitting the same ID returns the original result without creating a duplicateseasonIdstringBucket this score into a season (e.g. "season-3")teamIdstringBucket this score into a teamplayTimeMsnumberRun duration in millisecondsmetadataobjectAny extra JSON — stored and returned alongside the scoreFetch the leaderboard
Returns one entry per player (their best score) by default.
Filter by period, season, or team.
Use offset for pagination.
const { entries } = await lb.getLeaderboard({
limit: 10, // 1–100, default 20
offset: 0, // for pagination
period: 'weekly', // 'all' | 'weekly' | 'monthly'
season: 'season-3', // optional
team: 'team-red', // optional
});
entries.forEach(e => {
console.log(`#${e.rank} ${e.playerName} ${e.score}`);
});
// Pagination — load the next page
const page2 = await lb.getLeaderboard({ limit: 10, offset: 10 });Get a player's rank
Returns the player's rank, score, total player count, and percentile. percentile is 0–100 — higher means better. Rank 1 of 100 = percentile 100.
const data = await lb.getPlayerRank('player-123', {
period: 'weekly', // optional
});
console.log(data.rank); // 4
console.log(data.score); // 9500
console.log(data.totalPlayers); // 1024
console.log(data.percentile); // 99.7 — top 0.3% of players
// percentile is 0–100, higher = better. rank 1 of 100 → percentile 100More methods
Fetch the leaderboard centered on a specific player — the entries immediately above and below them. Great for in-game "you vs. your neighbours" screens.
// Shows the players immediately above and below the given player.
// Perfect for in-game "you vs. your neighbours" screens.
const { entries, playerRank } = await lb.getCentricLeaderboard('player-123', {
limit: 11, // 5 above + player row + 5 below
});Fetch or upsert a player profile. Fields are merged — omitted fields keep their current value.
// Fetch a player profile
const profile = await lb.getPlayer('player-123');
console.log(profile.playerName, profile.avatarUrl, profile.country);
// Create or update a profile — omitted fields keep their current value
await lb.updatePlayer('player-123', {
playerName: 'Alice',
avatarUrl: 'https://example.com/avatar.png',
country: 'US',
level: 42,
device: 'mobile',
});Full submission history for a player, ordered newest first. Use for progression charts or run-history screens.
// Full submission history for a player, newest first.
// Use this for progression charts or run-history screens.
const { entries, bestScore, total } = await lb.getPlayerScores('player-123', {
limit: 50, // 1–200, default 50
offset: 0,
});
console.log(`${total} runs, personal best: ${bestScore}`);Score signing (anti-cheat)
Enable "Require signed scores" in your game's dashboard.
Pass scoreSecret to the constructor and the SDK signs every submission
automatically using HMAC-SHA256 via the Web Crypto API — no extra code needed.
// Enable score signing in your game's dashboard, then pass scoreSecret:
const lb = new LightLeaderboard({
apiKey: process.env.LLB_API_KEY,
gameId: 'my_game',
scoreSecret: process.env.LLB_SCORE_SECRET, // from dashboard → Signature Secret
});
// Scores are now automatically HMAC-SHA256 signed — no extra code needed.
// Unsigned submissions will be rejected by the server.
await lb.submitScore({ score: 9500, playerRefId: 'player-123' });scoreSecret on the server alongside apiKey.
Unsigned submissions are rejected with 401 when signing is enabled.Error handling
All methods throw LightLeaderboardError on failure.
Use the typed helper flags to branch on common error cases.
import { LightLeaderboard, LightLeaderboardError } from 'lightleaderboard';
try {
await lb.submitScore({ score: 9500, playerRefId: 'player-123' });
} catch (err) {
if (err instanceof LightLeaderboardError) {
console.error(err.message); // human-readable message from the API
console.error(err.status); // HTTP status code
if (err.isAuthError) console.error('Check your API key');
if (err.isRateLimitError) console.error('Slow down score submissions');
if (err.isBillingError) console.error('Free tier limit — upgrade to Pro');
if (err.isValidationError) console.error('Invalid score data:', err.message);
}
}Complete Express server example
This is the pattern used in the KeyKeeper example project. The Express server holds the API key; the Kaboom.js client (running in the browser) calls your server — credentials never reach the client.
// server.js — Express example (safe: API key never touches the browser)
import express from 'express';
import { LightLeaderboard } from 'lightleaderboard';
const app = express();
app.use(express.json());
const lb = new LightLeaderboard({
apiKey: process.env.LLB_API_KEY,
gameId: 'my_game',
});
// Your game client POSTs here after each run
app.post('/submit', async (req, res) => {
const { score, playerId, playerName } = req.body;
const result = await lb.submitScore({
score,
playerRefId: playerId,
playerName,
submissionId: crypto.randomUUID(),
});
res.json(result); // { rank, isPersonalBest, totalPlayers }
});
app.get('/leaderboard', async (_req, res) => {
const data = await lb.getLeaderboard({ limit: 10, period: 'weekly' });
res.json(data.entries);
});
app.listen(3000);