Quickstart

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 scopes user:create, auth:code, balance:read and balance: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.

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 the pk_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

Try the live demo Pricing

Have questions about Poker Engine? We reply fast.