Add license validation to your browser game in under 10 minutes. Works with Canvas, WebGL, Phaser, Three.js, PixiJS, or any HTML5 game.
Copy the SDK file into your game folder, then add these 3 lines:
<script src="UnifiedLicensing.JavaScript.v4.0-Full.js"></script>
<script>
const manager = new UnifiedLicenseManager({
vendorApiKey: 'ul_YOUR_API_KEY',
productKey: 'YOUR_GAME_KEY'
});
const result = await manager.validateLicense('PLAYER-LICENSE-KEY');
if (result.valid) {
startGame();
} else {
showLicenseScreen();
}
</script>
That's it. The SDK handles caching, offline mode, and fingerprinting automatically.
ul_Copy UnifiedLicensing.JavaScript.v4.0-Full.js into your game folder. It's one file, no dependencies.
my-game/
index.html
game.js
UnifiedLicensing.JavaScript.v4.0-Full.js <-- put it here
assets/
Show a license input screen before the game loads. The game should not start until the player has a valid license.
Here's a simple license screen pattern:
<div id="license-screen">
<h2>Enter Your License Key</h2>
<input id="license-key" placeholder="XXXX-XXXX-XXXX">
<button onclick="activateLicense()">Activate</button>
<p id="license-error" style="color:red"></p>
</div>
<canvas id="game-canvas" style="display:none"></canvas>
<script>
async function activateLicense() {
const key = document.getElementById('license-key').value.trim();
const result = await manager.validateLicense(key, null, null, true);
if (result.valid) {
localStorage.setItem('game_license', key);
startGame();
} else {
document.getElementById('license-error').textContent = result.error || 'Invalid license';
}
}
</script>
When the page loads, check if the player already has a saved license. If yes, validate it. If not, show the license screen.
window.onload = async () => {
const savedKey = localStorage.getItem('game_license');
if (savedKey) {
const result = await manager.validateLicense(savedKey);
if (result.valid) {
startGame();
return;
}
// License expired or revoked - clear it
localStorage.removeItem('game_license');
}
showLicenseScreen();
};
When a player enters a license key:
validateLicense(key)localStorage and start the gameasync function activateLicense() {
const key = document.getElementById('license-key').value.trim();
if (!key) {
showError('Please enter a license key');
return;
}
const result = await manager.validateLicense(key, null, null, true);
if (result.valid) {
localStorage.setItem('game_license', key);
startGame();
} else if (result.quota_exceeded) {
showError('Service temporarily busy. Please try again in a minute.');
} else {
showError('Invalid license key. Please check and try again.');
}
}
Let players try your game before buying. Trials are tracked on the server, so players can't restart by clearing their browser data.
async function startTrial() {
const response = await fetch('https://api.unifiedlicensing.com/api/v1/start-trial', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
api_key: 'ul_YOUR_API_KEY',
product_key: 'YOUR_GAME_KEY',
machine_id: getMachineId(),
platform: 'web_client'
})
});
const data = await response.json();
if (data.trial_token) {
localStorage.setItem('trial_token', data.trial_token);
startGame();
showTrialBanner(data.remaining_days || 7);
}
}
function showTrialBanner(days) {
document.getElementById('trial-banner').textContent = `Trial: ${days} days remaining`;
document.getElementById('trial-banner').style.display = 'block';
}
// On page load, after checking for license
const trialToken = localStorage.getItem('trial_token');
if (trialToken) {
const response = await fetch('https://api.unifiedlicensing.com/api/v1/check-trial', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
api_key: 'ul_YOUR_API_KEY',
product_key: 'YOUR_GAME_KEY',
trial_token: trialToken,
machine_id: getMachineId(),
platform: 'web_client'
})
});
const data = await response.json();
if (data.valid) {
startGame();
showTrialBanner(data.remaining_days);
} else {
showLicenseScreen(); // Trial expired, show license input
}
}
// Start game after license validated
const game = new Phaser.Game({ /* your config */ });
// Pause until licensed
game.scene.scenes.forEach(scene => scene.scene.pause());
// After license validation succeeds:
function startGame() {
document.getElementById('license-screen').style.display = 'none';
document.getElementById('game-canvas').style.display = 'block';
game.scene.scenes.forEach(scene => scene.scene.resume());
}
let renderer, scene, camera;
let gameRunning = false;
function initGame() {
renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('game-canvas') });
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
gameRunning = true;
animate();
}
function animate() {
if (!gameRunning) return;
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
// Call initGame() only after license is validated
const canvas = document.getElementById('game-canvas');
const ctx = canvas.getContext('2d');
function startGame() {
document.getElementById('license-screen').style.display = 'none';
canvas.style.display = 'block';
gameLoop();
}
function gameLoop() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Your game rendering here
requestAnimationFrame(gameLoop);
}
A common question: "What if the player just deletes localStorage?"
Here's the key insight: localStorage is NOT the source of truth.
Here's a full working HTML page with license screen, trial, and a simple game:
<!DOCTYPE html>
<html>
<head>
<title>My Licensed Game</title>
<style>
body { margin: 0; background: #1a1a2e; color: #EFEAE0; font-family: system-ui; }
#license-screen { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; }
#license-screen input { padding: 12px; font-size: 18px; width: 300px; margin: 10px; border-radius: 8px; border: 1px solid #C7A34C; background: #14263B; color: #EFEAE0; }
#license-screen button { padding: 12px 24px; font-size: 16px; background: #C7A34C; color: #081220; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; }
#license-screen button:hover { background: #E4C468; }
#trial-banner { display: none; position: fixed; top: 0; left: 0; right: 0; background: #C7A34C; color: #081220; text-align: center; padding: 8px; font-weight: bold; z-index: 100; }
#game-canvas { display: none; margin: 0 auto; }
.error { color: #B5502E; margin-top: 8px; }
.links { margin-top: 20px; }
.links a { color: #C7A34C; margin: 0 10px; cursor: pointer; }
</style>
</head>
<body>
<div id="trial-banner"></div>
<div id="license-screen">
<h1>My Game</h1>
<p>Enter your license key to play</p>
<input id="license-key" placeholder="XXXX-XXXX-XXXX">
<button onclick="activateLicense()">Play</button>
<div class="links">
<a onclick="startTrial()">Start Free Trial</a>
</div>
<p id="error" class="error"></p>
</div>
<canvas id="game-canvas" width="800" height="600"></canvas>
<script src="UnifiedLicensing.JavaScript.v4.0-Full.js"></script>
<script>
const manager = new UnifiedLicenseManager({
vendorApiKey: 'ul_YOUR_API_KEY',
productKey: 'YOUR_GAME_KEY'
});
// On load: check for saved license or trial
window.onload = async () => {
const savedKey = localStorage.getItem('game_license');
if (savedKey) {
const result = await manager.validateLicense(savedKey);
if (result.valid) { startGame(); return; }
localStorage.removeItem('game_license');
}
const trialToken = localStorage.getItem('trial_token');
if (trialToken) {
// Check trial with server...
// If valid, startGame()
}
};
async function activateLicense() {
const key = document.getElementById('license-key').value.trim();
const result = await manager.validateLicense(key, null, null, true);
if (result.valid) {
localStorage.setItem('game_license', key);
startGame();
} else {
document.getElementById('error').textContent = result.error || 'Invalid license';
}
}
async function startTrial() {
const resp = await fetch('https://api.unifiedlicensing.com/api/v1/start-trial', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
api_key: 'ul_YOUR_API_KEY',
product_key: 'YOUR_GAME_KEY',
machine_id: navigator.userAgent.substring(0, 16),
platform: 'web_client'
})
});
const data = await resp.json();
if (data.trial_token) {
localStorage.setItem('trial_token', data.trial_token);
startGame();
}
}
function startGame() {
document.getElementById('license-screen').style.display = 'none';
const canvas = document.getElementById('game-canvas');
canvas.style.display = 'block';
const ctx = canvas.getContext('2d');
// Simple demo game - a bouncing ball
let x = 400, y = 300, dx = 3, dy = 2;
function loop() {
ctx.fillStyle = '#0E1B2C';
ctx.fillRect(0, 0, 800, 600);
ctx.beginPath();
ctx.arc(x, y, 20, 0, Math.PI * 2);
ctx.fillStyle = '#C7A34C';
ctx.fill();
x += dx; y += dy;
if (x < 20 || x > 780) dx = -dx;
if (y < 20 || y > 580) dy = -dy;
requestAnimationFrame(loop);
}
loop();
}
</script>
</body>
</html>
If you host on itch.io, the game runs inside an iframe. Keep these things in mind:
HTML5 games are fully client-side — the player's browser runs your code. No check is uncrackable, but you can make piracy annoying enough that most people pay.
Don't check the license once at the top. Check before each premium level or feature:
function loadLevel(levelId) {
const level = levels[levelId];
if (level.premium && !gameLicense.valid) {
showUpgradeScreen();
return;
}
startLevel(level);
}
The strongest protection: don't put premium assets in the download. Load them from YOUR server only after validation:
// Only download premium assets if license is valid
if (gameLicense.valid) {
const premiumAssets = await fetch('https://api.yourgame.com/assets/premium', {
headers: { 'Authorization': `Bearer ${gameLicense.key}` }
});
// Load premium levels, characters, etc.
}
Players can edit localStorage directly. Don't store save data that can be trivially modified:
// BAD — player can edit this
localStorage.setItem('player_coins', '999999');
// BETTER — validate server-side
const balance = await fetch('/api/balance', { headers: { auth: licenseKey } });
| Layer | What it stops | Effort to bypass |
|---|---|---|
| Per-level gating | Removing initial check | Rewrite all gates |
| Server-side assets | Copying the download | Reverse-engineer your API |
| Server-side state | localStorage edits | Fake server responses |
| Device fingerprinting | Account sharing | Spoofer fingerprint |
For HTML5 games: server-side asset delivery is the real moat. Everything else slows down casual pirates. If your game is premium enough, consider a thin server backend that serves levels dynamically.
| Problem | Fix |
|---|---|
| CORS error in console | Make sure you're calling the full URL: https://api.unifiedlicensing.com/api/v1/validate-license. Don't use relative paths. |
| "Invalid license" for valid key | Check that your vendorApiKey and productKey match what's in the dashboard. |
| Game loads before license check | Put your license check in window.onload, not inline. Make sure the canvas starts with display:none. |
| Quota exceeded error | You've hit the daily API limit. The SDK will use cached data. Wait 24 hours or upgrade your plan. |
| Game runs without valid license | You're not checking the validation result before starting. Always gate startGame() behind a successful validation. |
| Trial restarted after clearing browser | This shouldn't happen. Trials are tracked by device fingerprint on the server. If it does, your platform might be set wrong. |