This guide explains how to authenticate users from your platform directly into the poker client using single-use login codes. The SSO flow has two modes: cookie mode (a browser session on the poker platform) and token mode (JWT tokens for your own code to use).
Required scope: auth:code
How SSO Works
The SSO flow involves two separate HTTP requests made by two different parties:
- Your backend requests a one-time login code from the poker engine (HMAC-signed).
- The user’s browser exchanges that code for a full session on the poker platform (public endpoint).
┌──────────────────┐ ┌──────────────────┐
│ Partner Backend │ │ Poker Engine │
└────────┬─────────┘ └────────┬─────────┘
│ │
│ 1. POST /developer/auth/code │
│ (HMAC-signed, auth:code scope) │
│ ─────────────────────────────────────────► │
│ │
│ ◄──── { code: "a1b2c3...", expiresIn: 60 }
│ │
│ 2. Pass code to frontend │
│ (URL param, postMessage, API, etc.) │
▼ │
┌──────────────────┐ │
│ Partner Frontend │ │
│ (User's Browser) │ │
└────────┬─────────┘ │
│ │
│ 3. POST /auth/exchange │
│ (public — code IS the credential) │
│ ─────────────────────────────────────────► │
│ │
│ ◄──── Session (cookies or tokens) │
│ │
│ 4. User is now authenticated │
▼ │
┌──────────────────┐ │
│ Poker Platform │ │
│ (Lobby / Game) │ │
└──────────────────┘ │
Key points:
- The login code is a 64-character hex string, valid for 60 seconds, single-use.
- The code is hashed (SHA-256) before storage — the plaintext is never persisted.
- The exchange endpoint (
POST /auth/exchange) is public and rate-limited (10 per minute per IP). - The exchange endpoint requires the
developerApifeature toggle to be enabled. - The mode is fixed when the code is generated. The exchange request is the same for both, and the response follows the code’s mode.
Cookie Mode vs Token Mode
Choose a mode when generating the login code. The mode determines how the session is established after code exchange.
| Feature | Cookie Mode | Token Mode |
|---|---|---|
| Use case | Sign the user in to the poker web client | Call poker APIs from your own code |
| Session delivery | access_token and refresh_token HttpOnly cookies | JWT tokens returned in response body |
| Cross-site | Cookies are SameSite=Strict, so your site and the poker platform must be on the same site. Your origin must also be an allowed CORS origin | No cookies. Browser callers need an allowed CORS origin |
| Frontend code | Redirect to the poker URL after exchange | Store tokens, send them as Authorization: Bearer <accessToken> |
| Security | Cookies are HttpOnly, SameSite=Strict, and Secure in production | Tokens must be stored securely by your code |
| Default | Yes (if mode is omitted) | Must be explicitly requested |
When to Use Cookie Mode
- Your integration sends the user to the poker platform in a new tab or the same window.
- The poker platform and your site share the same registrable domain (for example
www.example.comandpoker.example.com). - You want the simplest integration — session cookies are handled automatically by the browser.
When to Use Token Mode
- You need to manage authentication tokens programmatically, for example from your own client calling poker APIs.
- The poker platform is on a different site than yours, so
SameSite=Strictcookies can’t be set by a cross-site request.
Token mode does not sign the user in to the poker web client. The web client authenticates with its own cookies and has no way to accept tokens from a parent page, and the platform does not set up cross-origin framing, so embedding it in an iframe on another site is not a supported flow.
Step-by-Step: Cookie Mode
Step 1: Generate a Login Code (Backend)
Your backend requests a login code from the poker engine using HMAC-signed authentication.
// backend/generate-code.js
const { apiRequest } = require('./hmac-client'); // See quickstart.md
async function generateLoginCode(externalUserId) {
const result = await apiRequest('POST', '/developer/auth/code', {
externalUserId,
mode: 'cookie',
});
return result; // { code: "a1b2c3...", expiresIn: 60, mode: "cookie" }
}
If the user does not yet have a poker account, the code generation endpoint auto-provisions one. You can include optional user data:
const result = await apiRequest('POST', '/developer/auth/code', {
externalUserId: 'user_12345',
mode: 'cookie',
username: 'john_doe', // Optional: auto-provisions with this username
email: 'john@example.com', // Optional: auto-provisions with this email
firstName: 'John', // Optional
lastName: 'Doe', // Optional
});
Step 2: Pass the Code to Your Frontend
Deliver the code to the user’s browser. Common patterns:
- API endpoint: Your frontend fetches the code from your backend via a button click.
- Server-rendered page: Include the code in the page HTML as a data attribute or hidden input.
- WebSocket/SSE: Push the code to the frontend in real-time.
// Example: Frontend fetches a login code from your backend
async function getLoginCode() {
const response = await fetch('/api/poker/login-code', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: currentUser.id }),
});
const { code } = await response.json();
return code; // 64-char hex string
}
Step 3: Exchange the Code in the Browser
The user’s browser calls the poker platform’s exchange endpoint directly. On success, session cookies are set automatically.
// frontend/poker-login.js
const POKER_URL = 'https://poker.example.com';
async function loginToPoker(code) {
const response = await fetch(`${POKER_URL}/api/v1/auth/exchange`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include', // Required — tells the browser to accept cookies
body: JSON.stringify({ code }),
});
if (!response.ok) {
const error = await response.json();
// error.error.code will be: INVALID_CODE, CODE_EXPIRED, or CODE_ALREADY_USED
throw new Error(`Login failed: ${error.error.code} — ${error.error.message}`);
}
const data = await response.json();
// data.data.user contains the authenticated user's profile
console.log('Logged in as:', data.data.user.username);
return data.data;
}
Cookie mode response:
{
"success": true,
"data": {
"user": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"username": "john_doe",
"email": "john@example.com",
"firstName": "John",
"lastName": "Doe",
"group": { "id": "...", "name": "player", "permissions": "f" }
},
"expiresIn": 900
}
}
user is abridged here. It is the user’s profile record without credential fields, plus their group.
The browser now has HttpOnly session cookies. The user can navigate to the poker lobby.
Step 4: Redirect to the Poker Platform
After a successful exchange, redirect the user to the poker lobby:
async function playPoker() {
const code = await getLoginCode();
await loginToPoker(code);
// User is authenticated — redirect to the poker lobby
window.location.href = `${POKER_URL}/lobby`;
}
Complete Cookie Mode Example
<!DOCTYPE html>
<html>
<head><title>Play Poker</title></head>
<body>
<button id="play-btn">Play Poker</button>
<script>
const POKER_URL = 'https://poker.example.com';
document.getElementById('play-btn').addEventListener('click', async () => {
try {
// Step 1: Get a login code from YOUR backend
const codeRes = await fetch('/api/poker/login-code', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: 'current_user_id' }),
});
const { code } = await codeRes.json();
// Step 2: Exchange the code on the poker platform
const exchangeRes = await fetch(`${POKER_URL}/api/v1/auth/exchange`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ code }),
});
if (!exchangeRes.ok) {
const err = await exchangeRes.json();
alert(`Login failed: ${err.error.message}`);
return;
}
// Step 3: Redirect to the poker lobby
window.location.href = `${POKER_URL}/lobby`;
} catch (err) {
console.error('Poker login error:', err);
alert('Could not connect to poker platform. Please try again.');
}
});
</script>
</body>
</html>
Step-by-Step: Token Mode
Token mode is designed for integrations that call poker APIs themselves, in environments where cookies cannot be shared.
Step 1: Generate a Login Code (Backend)
Same as cookie mode, but with mode: 'token':
const result = await apiRequest('POST', '/developer/auth/code', {
externalUserId: 'user_12345',
mode: 'token',
});
// { code: "a1b2c3...", expiresIn: 60, mode: "token" }
Step 2: Exchange the Code for Tokens
The exchange endpoint returns JWT tokens in the response body (no cookies are set).
const POKER_URL = 'https://poker.example.com';
async function exchangeForTokens(code) {
const response = await fetch(`${POKER_URL}/api/v1/auth/exchange`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
// No `credentials: 'include'` needed — tokens come in the body
body: JSON.stringify({ code }),
});
if (!response.ok) {
const { error } = await response.json();
throw new Error(`Exchange failed: ${error.code}`);
}
const data = await response.json();
return data.data;
// {
// accessToken: "eyJhbGciOi...",
// refreshToken: "eyJhbGciOi...",
// expiresIn: 900,
// user: { id, username, email, ... }
// }
}
Token mode response:
{
"success": true,
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 900,
"user": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"username": "john_doe",
"email": "john@example.com",
"firstName": "John",
"lastName": "Doe",
"group": { "id": "...", "name": "player", "permissions": "f" }
}
}
}
Step 3: Use the Tokens
Send the access token as a bearer token on authenticated poker API calls. expiresIn is the access token’s lifetime in seconds.
const res = await fetch(`${POKER_URL}/api/v1/auth/me`, {
headers: { Authorization: `Bearer ${tokens.accessToken}` },
});
When the access token expires, send the refresh token in the body of POST /api/v1/auth/refresh as { "refreshToken": "..." }.
Auto-Provisioning
The POST /developer/auth/code endpoint automatically provisions a poker account if the user does not already have one mapped to the given externalUserId. This means you can skip the explicit user provisioning step (POST /developer/users) and go straight to generating login codes.
// This single call provisions the user AND generates a login code
const result = await apiRequest('POST', '/developer/auth/code', {
externalUserId: 'new_user_999',
mode: 'cookie',
username: 'new_player',
email: 'newplayer@example.com',
});
// User is created, code is returned — ready for exchange
If the user already exists (same externalUserId for your partner), a code is generated for the existing account.
Error Handling
Code Generation Errors
| Error Code | HTTP Status | Cause | Resolution |
|---|---|---|---|
MODE_NOT_ALLOWED | 400 | Token or cookie mode is disabled in developer settings | Contact the platform admin to enable the desired mode |
MISSING_USERNAME | 400 | The user is new and the policy needs a value that wasn’t sent: username under require, or username or email under generate_from_email | Include the field in the request body |
EMAIL_CONFLICT | 409 | Email already exists for a different user | Use a different email or provision without email |
USERNAME_CONFLICT | 409 | Username is already taken | Choose a different username |
These use the flat format { "error": "CODE", "message": "..." }.
Code Exchange Errors
Exchange errors use the nested format: { "success": false, "error": { "code": "CODE", "message": "..." } }.
| Error Code | HTTP Status | Cause | Resolution |
|---|---|---|---|
INVALID_CODE | 401 | The code does not exist or was never generated | Verify you are sending the correct 64-character hex code |
CODE_EXPIRED | 401 | The code is older than 60 seconds | Generate a new code — they expire quickly for security |
CODE_ALREADY_USED | 401 | The code has already been exchanged | Generate a new code — codes are single-use |
VALIDATION_ERROR | 400 | The code is not exactly 64 lowercase hex characters | Send the code exactly as returned |
ACCOUNT_BLOCKED | 403 | The user’s account is blocked. The code is used up | Contact the platform admin |
ACCOUNT_FROZEN | 403 | The user’s account is frozen. The code is used up | Contact the platform admin |
AUTHORIZATION_ERROR | 403 | The developerApi feature toggle is disabled | Contact the platform admin |
FEATURE_TIER_RESTRICTED | 403 | The platform’s license tier does not include the Developer API | Contact the platform admin |
RATE_LIMIT_EXCEEDED | 429 | Too many exchange attempts from this IP | Wait 1 minute before retrying |
Handling Errors in Code
async function safeExchange(code) {
const response = await fetch(`${POKER_URL}/api/v1/auth/exchange`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ code }),
});
if (!response.ok) {
const { error } = await response.json();
switch (error.code) {
case 'CODE_EXPIRED':
case 'CODE_ALREADY_USED':
// Code is no longer valid — request a new one from your backend
console.log('Code invalid, requesting new code...');
return { retry: true };
case 'INVALID_CODE':
// Something went wrong with code transmission
console.error('Invalid code — check your backend integration');
return { error: 'Invalid login code' };
case 'RATE_LIMIT_EXCEEDED':
console.warn('Rate limited — wait before retrying');
return { error: 'Too many attempts, please wait' };
default:
console.error('Unexpected error:', error);
return { error: 'Login failed' };
}
}
return await response.json();
}
Security Considerations
Codes Are Single-Use
Each login code can only be exchanged once. After a successful exchange, the code is marked as used and cannot be reused. This prevents session hijacking even if the code is intercepted.
Codes Expire in 60 Seconds
Login codes are intentionally short-lived. Generate a code only when the user is ready to be authenticated, not in advance. If the code expires, generate a new one.
Never Put Codes in URLs
Do not pass login codes as URL query parameters (e.g., ?code=abc123). URL parameters are logged by browsers, web servers, proxies, and analytics tools. Instead:
- Pass the code via a POST request body from your frontend to the exchange endpoint.
- If you need to transfer the code between pages, use
sessionStorage, a hidden form field, orpostMessage.
The Exchange Endpoint Is Public
POST /auth/exchange does not require HMAC authentication — the code itself is the credential. This is by design: the request comes from the user’s browser, which does not have access to your API secret.
The endpoint is protected by:
- Rate limiting: 10 attempts per minute per IP.
- Code expiry: 60-second validity window.
- Single use: Codes cannot be reused after exchange.
- Feature toggle: The endpoint returns 403 when the
developerApifeature toggle is disabled.
CORS Configuration
For cookie mode, the poker platform must have CORS configured to allow your origin, and your site must be on the same site as the platform because the cookies are SameSite=Strict. Contact the platform admin to add your domain to the allowed origins list.
For token mode, CORS must allow your origin but credentials are not required (tokens are in the response body).
Token Storage (Token Mode)
When using token mode, you are responsible for securely storing the JWT tokens:
- Do not store tokens in
localStorageif XSS is a concern — usesessionStorageor in-memory storage. - Refresh tokens can be used to obtain new access tokens when they expire.
- Access tokens are short-lived. Use
expiresInrather than hard-coding a lifetime, since the platform configures it.
Next Steps
- User Provisioning — Learn about username policies and email handling
- Balance Operations — Credit and debit user balances
- Authentication — HMAC signing details
- Quickstart — End-to-end integration in 5 minutes
- Error Reference — Complete list of all error codes