This guide walks you through making your first Developer API calls: provisioning a user, generating a login code, and exchanging it for a session.
Prerequisites:
- An API key (
pk_live_...) and secret (sk_live_...) from the platform admin or self-registration, with the scopesuser:create,auth:code,balance:readandbalance:write - Node.js 18+ installed (for the code examples)
- The poker platform base URL (e.g.,
https://poker.example.com)
Step 1: Set Up the HMAC Signing Helper
Every API call must be HMAC-SHA256 signed. Save this helper — you will reuse it for all requests.
// hmac-client.js
const crypto = require('crypto');
const API_KEY = 'pk_live_YOUR_KEY_HERE';
const API_SECRET = 'sk_live_YOUR_SECRET_HERE';
const BASE_URL = 'https://poker.example.com';
function canonicalPathAndQuery(url) {
const parsed = new URL(url);
const pairs = [...parsed.searchParams.entries()].sort(([ak, av], [bk, bv]) =>
ak === bk ? av.localeCompare(bv) : ak.localeCompare(bk)
);
const query = pairs
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
return query ? `${parsed.pathname}?${query}` : parsed.pathname;
}
async function apiRequest(method, path, body = null) {
const timestamp = Date.now().toString();
const requestId = crypto.randomUUID();
const bodyStr = body ? JSON.stringify(body) : '';
const url = `${BASE_URL}/api/v1${path}`;
const signingString = [
'v1',
method.toUpperCase(),
canonicalPathAndQuery(url),
timestamp,
requestId,
bodyStr,
].join('\n');
const signature = crypto
.createHmac('sha256', API_SECRET)
.update(signingString)
.digest('hex');
const headers = {
'X-API-Key': API_KEY,
'X-Signature': signature,
'X-Timestamp': timestamp,
'X-Request-Id': requestId,
'Content-Type': 'application/json',
};
const response = await fetch(url, {
method,
headers,
body: bodyStr || undefined,
});
if (response.status === 204) {
return null; // DELETE responses have no body
}
const data = await response.json();
if (!response.ok) {
throw new Error(`API error ${response.status}: ${data.error} — ${data.message}`);
}
return data;
}
module.exports = { apiRequest };
Step 2: Provision a User
Create a poker account linked to a user on your platform. This call is idempotent — calling it again with the same externalUserId returns the existing account.
// provision-user.js
const { apiRequest } = require('./hmac-client');
async function main() {
const result = await apiRequest('POST', '/developer/users', {
externalUserId: 'user_12345',
username: 'john_doe',
email: 'john@example.com',
firstName: 'John',
lastName: 'Doe',
});
console.log('User provisioned:', result.user.id);
console.log('Username:', result.user.username);
console.log('Is new account:', result.isNew);
}
main().catch(console.error);
Expected response (201 Created):
{
"user": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"externalUserId": "user_12345",
"username": "john_doe",
"email": "john@example.com",
"firstName": "John",
"lastName": "Doe",
"balance": "0",
"virtualBalance": "0",
"createdAt": "2026-02-18T12:00:00.000Z"
},
"isNew": true
}
Step 3: Generate a Login Code
Request a single-use code that your frontend can exchange for a poker session. Codes expire after 60 seconds.
// generate-login-code.js
const { apiRequest } = require('./hmac-client');
async function main() {
const result = await apiRequest('POST', '/developer/auth/code', {
externalUserId: 'user_12345',
mode: 'cookie', // or 'token' to receive bearer tokens
});
console.log('Login code:', result.code);
console.log('Expires in:', result.expiresIn, 'seconds');
console.log('Mode:', result.mode);
// Pass this code to your frontend immediately — it expires in 60 seconds
}
main().catch(console.error);
Expected response (201 Created):
{
"code": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456",
"expiresIn": 60,
"mode": "cookie"
}
Step 4: Exchange the Code in the Browser
This request is made from the user’s browser (not your backend). It is a public endpoint — the code itself is the credential.
Cookie Mode (Redirect-Based SSO)
The user’s browser calls the exchange endpoint directly. On success, session cookies are set automatically. The cookies are SameSite=Strict, so your site and the poker platform must be on the same site (for example www.example.com and poker.example.com), and your origin must be in the platform’s allowed CORS origins.
<!-- exchange.html -->
<script>
async function loginWithCode(code) {
const response = await fetch('https://poker.example.com/api/v1/auth/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include', // Required for cookies
body: JSON.stringify({ code }),
});
if (!response.ok) {
const { error } = await response.json();
console.error('Login failed:', error.code, error.message);
return;
}
const data = await response.json();
console.log('Logged in as:', data.data.user.username);
// Redirect to the poker lobby
window.location.href = 'https://poker.example.com/lobby';
}
// Get the code from your backend (Step 3) and call this function
// loginWithCode('a1b2c3d4e5f6...');
</script>
Token Mode (Bearer Tokens)
Use token mode to receive JWT tokens in the response body instead of cookies, then send them as Authorization: Bearer <accessToken>. The mode is fixed when the code is generated in Step 3; the exchange request is the same for both.
let session = null;
async function loginWithToken(code) {
const response = await fetch('https://poker.example.com/api/v1/auth/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code }),
});
if (!response.ok) {
const { error } = await response.json();
throw new Error(`Login failed: ${error.code}`);
}
const data = await response.json();
const { accessToken, refreshToken, expiresIn } = data.data;
// Keep the tokens in memory and use them for subsequent API calls
session = { accessToken, refreshToken };
console.log('Authenticated. Token expires in', expiresIn, 'seconds');
}
Complete Example: All Steps in One Script
Save this as quickstart.js and run with node quickstart.js:
// quickstart.js — Complete integration demo
const crypto = require('crypto');
// ─── Configuration ───────────────────────────────────────────
const API_KEY = 'pk_live_YOUR_KEY_HERE';
const API_SECRET = 'sk_live_YOUR_SECRET_HERE';
const BASE_URL = 'https://poker.example.com';
// ─────────────────────────────────────────────────────────────
function canonicalPathAndQuery(url) {
const parsed = new URL(url);
const pairs = [...parsed.searchParams.entries()].sort(([ak, av], [bk, bv]) =>
ak === bk ? av.localeCompare(bv) : ak.localeCompare(bk)
);
const query = pairs
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
return query ? `${parsed.pathname}?${query}` : parsed.pathname;
}
async function apiRequest(method, path, body = null) {
const timestamp = Date.now().toString();
const requestId = crypto.randomUUID();
const bodyStr = body ? JSON.stringify(body) : '';
const url = `${BASE_URL}/api/v1${path}`;
const signature = crypto
.createHmac('sha256', API_SECRET)
.update([
'v1',
method.toUpperCase(),
canonicalPathAndQuery(url),
timestamp,
requestId,
bodyStr,
].join('\n'))
.digest('hex');
const res = await fetch(url, {
method,
headers: {
'X-API-Key': API_KEY,
'X-Signature': signature,
'X-Timestamp': timestamp,
'X-Request-Id': requestId,
'Content-Type': 'application/json',
},
body: bodyStr || undefined,
});
if (res.status === 204) return null;
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.error}: ${data.message}`);
return data;
}
async function main() {
// Step 1: Provision a user
console.log('Provisioning user...');
const user = await apiRequest('POST', '/developer/users', {
externalUserId: 'demo_user_001',
username: 'demo_player',
email: 'demo@example.com',
});
console.log(` User: ${user.user.username} (${user.isNew ? 'new' : 'existing'})`);
// Step 2: Check their balance
console.log('\nChecking balance...');
const balance = await apiRequest('GET', '/developer/users/demo_user_001/balance');
console.log(` Balance: $${balance.balance}`);
// Step 3: Credit some funds
console.log('\nCrediting $100...');
const credit = await apiRequest('POST', '/developer/users/demo_user_001/balance/credit', {
amount: '100.00',
reference: 'welcome_bonus_001',
description: 'Welcome bonus',
});
console.log(` New balance: $${credit.transaction.balanceAfter}`);
console.log(` Transaction ID: ${credit.transaction.id}`);
// Step 4: Generate a login code
console.log('\nGenerating login code...');
const auth = await apiRequest('POST', '/developer/auth/code', {
externalUserId: 'demo_user_001',
mode: 'cookie',
});
console.log(` Code: ${auth.code.substring(0, 12)}...`);
console.log(` Expires in: ${auth.expiresIn}s`);
console.log('\n Pass this code to your frontend to complete login.');
console.log(' POST /api/v1/auth/exchange { "code": "..." }');
}
main().catch(err => {
console.error('\nError:', err.message);
process.exit(1);
});
Troubleshooting
INVALID_SIGNATURE (401)
The HMAC signature does not match. Common causes:
- Wrong secret — Make sure you are using the
sk_live_...secret, not thepk_live_...key. - Wrong signing string — The signing string is newline-separated
v1, method, canonical path/query, timestamp, request ID, and raw body. For requests with no body, the final raw-body field is empty, so the string ends with a newline. - Stringified differently — The body in the signing string must be the exact JSON bytes sent in the request. Do not re-stringify.
TIMESTAMP_EXPIRED (401)
The X-Timestamp header is more than 5 minutes old. Ensure your server clock is synchronized (use NTP). The timestamp must be in Unix milliseconds (e.g., 1708264800000); a value in seconds is rejected with this error.
TIMESTAMP_FUTURE (401)
The X-Timestamp header is more than 30 seconds in the future. Check your server clock.
INVALID_API_KEY (401)
The API key is not recognized, has been revoked, has expired, or the partner account is not active/approved.
INSUFFICIENT_SCOPES (403)
The API key does not have the required scope for this endpoint. The response includes the missing scopes:
{
"error": "INSUFFICIENT_SCOPES",
"message": "API key is missing required scopes: balance:write",
"missingScopes": ["balance:write"]
}
RATE_LIMIT_EXCEEDED (429)
You have exceeded the rate limit for this API key. Wait and retry. The response includes a Retry-After header.
Next Steps
- Authentication Guide — Deep dive into HMAC signing with examples in multiple languages
- SSO Integration — Cookie mode vs. token mode, security considerations
- User Provisioning — Username policies, email handling, idempotency
- Balance Operations — Credits, debits, and idempotent transactions
- Webhooks — Subscribe to real-time events