PlayKit.ai

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:

CredentialUse CaseAuto UIWhere it lives
API keyDevelopment, backend, CINoYour machine or server — never in a shipped game
Device Auth FlowBrowser games (production runtime)YesThe player's running game
Headless Device AuthTerminal/CLI appsNoThe 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 key

Security: 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 needed

How It Works:

  1. SDK detects no valid token
  2. Shows login modal with "Login to Play" button
  3. User clicks button, opens PlayKit login page in new tab
  4. User completes login (email/phone verification)
  5. SDK polls for completion, receives token
  6. 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:

EnvironmentDefault StoragePersistence
Browser (mode: 'browser')localStoragePersistent
Server (mode: 'server')In-memorySession 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:

  1. Browser mode: SDK automatically shows login UI
  2. Server mode: Throws REFRESH_TOKEN_EXPIRED error
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' event

Error Handling

Error Codes

CodeDescriptionAction
ACCESS_DENIEDUser denied authorizationShow message, offer retry
EXPIREDSession or token expiredRestart auth flow
CANCELLEDAuth flow was cancelledUser action, no error needed
NOT_AUTHENTICATEDNo token availableStart login flow
AUTH_FAILEDToken validation failedRe-authenticate
AUTH_ERRORGeneral authentication errorCheck error message
NO_REFRESH_TOKENNo refresh token availableRe-authenticate
REFRESH_TOKEN_EXPIREDRefresh token has expiredRe-authenticate
REFRESH_TOKEN_INVALIDRefresh token is invalidRe-authenticate
TOKEN_EXPIREDAccess token expired, no refresh availableRe-authenticate
INSUFFICIENT_CREDITSNot 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.

ScopeDescriptionUse Case
fullUnrestricted access for your accountBackend, editor, CI
analytics:readRead-only access to usage and analyticsReporting jobs

Security Best Practices

  1. Never ship API keys in a game build - Keep them server-side; read them from environment variables
  2. Validate the player credential server-side before trusting user identity
  3. Use HTTPS in production environments
  4. Handle credential expiration gracefully - Listen for error events
  5. Don't store sensitive data alongside credentials in localStorage
  6. 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