Authentication
Understanding PlayKit SDK authentication methods and login flows
Authentication
This guide covers all authentication methods available in the PlayKit SDK, from development to production, and from browser to terminal environments.
Overview
PlayKit has one identity model — a single account, with no developer/player wall. What a credential can do is governed by its scopes, not by who you are. The SDK works with two kinds of credentials:
| Credential | Use Case | Auto UI | Where it lives |
|---|---|---|---|
| API key | Development, backend, CI | No | Your machine or server — never in a shipped game |
| Device Auth Flow | Browser games (production runtime) | Yes | The player's running game |
| Headless Device Auth | Terminal/CLI apps | No | The user's session |
API Key
For development, testing, and any server-side work, use an API key. Mint one in the playkit.ai dashboard and attach the scopes it needs — for example full for unrestricted calls. An API key skips all login UI and bills usage to your account.
Using an API key
Get a key from the playkit.ai dashboard and pass it directly:
const sdk = new PlayKitSDK({
gameId: 'your-game-id',
apiKey: 'your-api-key', // Minted in the playkit.ai dashboard, with scopes
});
await sdk.initialize();
// SDK is ready, using your API keySecurity: An API key is a server-side credential. Never commit it to version control, ship it inside a game build, or expose it in client-side code. Remove apiKey before deploying a game — at runtime, players authenticate themselves via device auth.
The SDK shows a red "API key" indicator when an API key is in use in a browser environment, so an accidentally shipped key is easy to spot.
Loading the key without hardcoding
Rather than pasting the key into source, read it from an environment variable that only exists on your machine or CI. This keeps the key out of the codebase and out of any shipped build:
const sdk = new PlayKitSDK({
gameId: 'your-game-id',
// Read from a local-only env var; undefined in shipped builds.
apiKey: process.env.PLAYKIT_API_KEY,
});
await sdk.initialize();When the key is absent — as it always is in a shipped game — the SDK falls back to device auth and prompts the player to sign in.
Production safety net
Add this check to warn if an API key is ever present in a browser running outside local development:
const isDevelopment = window.location.protocol === 'file:' ||
window.location.hostname === 'localhost' ||
window.location.hostname === '127.0.0.1';
if (typeof window !== 'undefined' && apiKey && !isDevelopment) {
alert('Warning: API key detected in a shipped game!');
}Player Authentication
For production games, players authenticate via the Device Auth Flow.
Device Auth Flow (Browser)
For production browser games, omit the apiKey. The SDK automatically shows a login modal when the player needs to authenticate via device auth.
const sdk = new PlayKitSDK({
gameId: 'your-game-id',
});
sdk.on('authenticated', (authState) => {
console.log('User logged in!', authState);
});
sdk.on('unauthenticated', () => {
console.log('User needs to log in');
});
await sdk.initialize(); // Shows login UI if neededHow It Works:
- SDK detects no valid token
- Shows login modal with "Login to Play" button
- User clicks button, opens PlayKit login page in new tab
- User completes login (email/phone verification)
- SDK polls for completion, receives token
- Token is saved locally for future sessions
Customizing the Flow:
const { authUrl, sessionId, codeVerifier, expiresIn } = await sdk.initiateLogin('player:play');
// Open in a custom popup
window.open(authUrl, 'playkit-login', 'width=500,height=600');
// Poll for completion
const result = await sdk.completeLogin(sessionId, codeVerifier, {
onStatus: (status) => console.log('Poll status:', status),
timeoutMs: expiresIn * 1000,
});Headless Device Auth (Terminal/CLI)
For terminal applications, CLI tools, or environments without browser popups, use headless device auth.
const sdk = new PlayKitSDK({
gameId: 'your-game-id',
mode: 'server',
});
// Step 1: Get the auth URL
const { authUrl, sessionId, codeVerifier, expiresIn } = await sdk.initiateLogin();
// Step 2: Display URL to user
console.log('Please visit this URL to log in:');
console.log(authUrl);
console.log(`\nThis link expires in ${Math.floor(expiresIn / 60)} minutes.`);
// Or generate a QR code
// const qr = require('qrcode-terminal');
// qr.generate(authUrl, { small: true });
// Step 3: Poll for completion
try {
const result = await sdk.completeLogin(sessionId, codeVerifier, {
onStatus: (status) => {
if (status === 'pending') {
process.stdout.write('.');
}
},
timeoutMs: expiresIn * 1000,
});
console.log('\nLogin successful!');
console.log('Token:', result.access_token);
} catch (error) {
if (error.code === 'ACCESS_DENIED') {
console.log('\nUser denied authorization');
} else if (error.code === 'EXPIRED') {
console.log('\nSession expired, please try again');
}
}Cancel Ongoing Login:
sdk.cancelLogin();Token Management
Understanding how tokens are stored, expire, and refresh is important for production applications.
Token Storage
The SDK provides a cross-platform storage abstraction that automatically adapts to the runtime environment:
| Environment | Default Storage | Persistence |
|---|---|---|
Browser (mode: 'browser') | localStorage | Persistent |
Server (mode: 'server') | In-memory | Session only |
Storage Format:
// Storage key format
`playkit_{gameId}_auth`
// Stored data structure (AES-128-GCM encrypted in browser)
{
isAuthenticated: boolean;
token: string;
expiresAt: number; // Unix timestamp in milliseconds
}Custom Storage Provider:
Implement the IStorage interface to use custom storage backends (Redis, database, etc.):
import { PlayKitSDK, IStorage } from 'playkit-sdk';
class RedisStorage implements IStorage {
private client: RedisClient;
getItem(key: string): string | null {
return this.client.get(key);
}
setItem(key: string, value: string): void {
this.client.set(key, value);
}
removeItem(key: string): void {
this.client.del(key);
}
keys(): string[] {
return this.client.keys('playkit_*');
}
}
// Usage with custom storage
const sdk = new PlayKitSDK({
gameId: 'your-game-id',
playerToken: token,
mode: 'server',
});
await sdk.initialize();In server mode, tokens are stored in memory by default and do not persist across process restarts. For production backends, pass tokens directly via playerToken configuration or implement a custom storage provider.
Token Expiration
Player credentials have a default expiration of 24 hours. Refresh tokens are valid for 30 days.
Check Expiration:
// Check if access token is expired
if (sdk.isTokenExpired()) {
console.log('Access token has expired');
}
// Get expiration time
const authState = sdk.getAuthState();
if (authState.expiresAt) {
const expiresIn = authState.expiresAt - Date.now();
console.log(`Access token expires in ${Math.floor(expiresIn / 1000 / 60)} minutes`);
}
// Check refresh token availability
if (authState.refreshToken) {
console.log('Refresh token available');
}Token Refresh
The SDK supports token refresh using refresh tokens obtained during authentication.
Browser Mode (Automatic):
In browser mode, the SDK checks token validity before API calls. This is a lightweight timestamp comparison with no performance impact. Only when the token expires within 5 minutes does the SDK make a refresh API call.
// SDK checks: is token expiring soon?
// - If no: proceeds immediately (no latency)
// - If yes: refreshes first, then proceeds
const response = await chatClient.chat('Hello');Manual Refresh:
For explicit control or server-side applications:
// Check if refresh is possible
if (sdk.canRefreshToken()) {
const result = await sdk.refreshToken();
console.log('New token expires in:', result.expiresIn, 'seconds');
}Server Mode:
In server mode, automatic refresh is disabled. Manage token refresh explicitly:
const sdk = new PlayKitSDK({
gameId: 'your-game-id',
mode: 'server',
playerToken: storedToken,
});
await sdk.initialize();
// Refresh manually when needed
if (sdk.isTokenExpired() && sdk.canRefreshToken()) {
const result = await sdk.refreshToken();
saveTokens(result.access_token, result.refresh_token);
}Refresh Events:
// Listen for successful token refresh
sdk.on('token_refreshed', (authState) => {
console.log('Token refreshed, new expiry:', new Date(authState.expiresAt));
});Automatic Re-authentication
When token refresh fails or refresh token expires:
- Browser mode: SDK automatically shows login UI
- Server mode: Throws
REFRESH_TOKEN_EXPIREDerror
sdk.on('error', (error) => {
if (error.code === 'REFRESH_TOKEN_EXPIRED') {
console.log('Session expired, please re-authenticate');
}
});Manual Re-authentication
Force re-authentication when needed:
await sdk.logout();
// Re-initialize — SDK will show login UI (browser) or require initiateLogin (server)
await sdk.initialize();Token Validation
Validate tokens server-side by fetching player info:
// Fetches player info and validates the token (also triggers daily credits refresh)
const playerInfo = await sdk.getPlayerInfo();
console.log(playerInfo);
// { userId, balance, dailyRefresh, ... }Backend Services
For backend services that need to make AI calls, you have two patterns depending on whose balance pays.
Using your API key
To make calls billed to your own account — for example a server-side feature that is free to the player — initialize with your scoped API key:
const sdk = new PlayKitSDK({
gameId: 'your-game-id',
apiKey: process.env.PLAYKIT_API_KEY, // Server-side only
mode: 'server',
});
await sdk.initialize();
const chat = sdk.createChatClient('gpt-4o-mini');
const response = await chat.chat('Hello');Acting on behalf of a player
To make calls billed to a specific player, forward the player credential that the client obtained via device auth, and pass it to the SDK in server mode:
const sdk = new PlayKitSDK({
gameId: 'your-game-id',
playerToken: playerCredentialFromRequest, // Forwarded from the client's device-auth session
mode: 'server',
});
await sdk.initialize();
const chat = sdk.createChatClient('gpt-4o-mini');
const response = await chat.chat('Hello');See Backend Services for complete backend integration guide.
Authentication State
Check Status
// Check if authenticated
if (sdk.isAuthenticated()) {
console.log('User is logged in');
}
// Get current token
const token = sdk.getToken();
// Get full auth state
const authState = sdk.getAuthState();
console.log(authState);
// { isAuthenticated: true, token: '...', expiresAt: 1234567890 }Events
// Successful authentication
sdk.on('authenticated', (authState) => {
console.log('Authenticated:', authState.isAuthenticated);
});
// Session ended
sdk.on('unauthenticated', () => {
console.log('User logged out or session expired');
});
// Authentication error (token invalid, expired, etc.)
sdk.on('error', (error) => {
console.error('Auth error:', error.message, error.code);
});
// Token refreshed (access token renewed via refresh token)
sdk.on('token_refreshed', (authState) => {
console.log('Token refreshed, new expiry:', authState.expiresAt);
});
// Daily credits refreshed (happens on first API call each day)
sdk.on('daily_credits_refreshed', (data) => {
console.log(`Added ${data.amountAdded} credits!`);
});Logout
await sdk.logout();
// Clears token, emits 'unauthenticated' eventError Handling
Error Codes
| Code | Description | Action |
|---|---|---|
ACCESS_DENIED | User denied authorization | Show message, offer retry |
EXPIRED | Session or token expired | Restart auth flow |
CANCELLED | Auth flow was cancelled | User action, no error needed |
NOT_AUTHENTICATED | No token available | Start login flow |
AUTH_FAILED | Token validation failed | Re-authenticate |
AUTH_ERROR | General authentication error | Check error message |
NO_REFRESH_TOKEN | No refresh token available | Re-authenticate |
REFRESH_TOKEN_EXPIRED | Refresh token has expired | Re-authenticate |
REFRESH_TOKEN_INVALID | Refresh token is invalid | Re-authenticate |
TOKEN_EXPIRED | Access token expired, no refresh available | Re-authenticate |
INSUFFICIENT_CREDITS | Not enough balance (402) | Show upgrade prompt |
Example
try {
await sdk.initialize();
} catch (error) {
switch (error.code) {
case 'ACCESS_DENIED':
showMessage('Please authorize to play.');
break;
case 'NOT_AUTHENTICATED':
console.error('Please provide a token');
break;
case 'AUTH_FAILED':
showMessage('Session expired. Please login again.');
await sdk.logout();
await sdk.initialize();
break;
default:
console.error('Initialization failed:', error);
}
}Scopes Reference
Scopes are attached to API keys when you mint them in the dashboard, and they govern what the key may do. The player credential issued by device auth carries only the access needed to make AI calls on the signed-in player's behalf.
| Scope | Description | Use Case |
|---|---|---|
full | Unrestricted access for your account | Backend, editor, CI |
analytics:read | Read-only access to usage and analytics | Reporting jobs |
Security Best Practices
- Never ship API keys in a game build - Keep them server-side; read them from environment variables
- Validate the player credential server-side before trusting user identity
- Use HTTPS in production environments
- Handle credential expiration gracefully - Listen for
errorevents - Don't store sensitive data alongside credentials in localStorage
- Implement logout on security-sensitive operations
Prepare for Release
For distribution, omit the apiKey from the production configuration — the SDK then authenticates each player via device auth and presents the login flow automatically. See Prepare for Release for the full checklist.
Next Steps
- Backend Services - Server-side token validation and AI calls
- Text Generation - Start generating AI content
- NPC Conversations - Build intelligent game characters