Integration API

The Developer Integration API allows external platforms to embed the poker engine into their products. Operators can create player accounts, authenticate users via SSO, read balances, and receive real-time event notifications via webhooks.

Architecture Overview

External Platform
      │
      ▼
API Key + HMAC signature
      │
      ▼
Partner routes  (src/api/routes/developer/)
      ├── User provisioning  (developer-user.service.ts)
      ├── SSO login codes    (developer-user.service.ts)
      ├── Balance operations (developer-balance.service.ts)
      ├── Webhook subs       (developer-webhook.service.ts)
      └── Self-registration  (developer-partner.service.ts)

Code exchange   (POST /api/v1/auth/exchange, src/api/routes/auth.routes.ts)

Admin routes    (src/api/routes/admin/developer.routes.ts)
      ├── Partner CRUD
      ├── API key management
      └── Webhook delivery monitoring

Getting Started

1. Enable the feature

The Developer API is gated by the developerApi feature toggle and is part of the Enterprise license tier. Enable Developer API under Feature Toggles in Admin Panel → Settings. While it is off, every partner endpoint except the documentation pages returns 403.

2. Create a partner

In the Admin Panel go to Developer API → Partners → Add Partner. Or via the admin API:

POST /api/v1/admin/developer/partners
Authorization: Bearer <admin-jwt>
Content-Type: application/json

{
  "name": "My Platform",
  "contactEmail": "dev@myplatform.com",
  "websiteUrl": "https://myplatform.com",
  "description": "Platform description"
}

Admin-created partners are auto-approved. Partners that self-register (if selfRegistrationEnabled is on) need admin approval unless requireAdminApproval is off.

3. Generate an API key

On the partner’s page use Generate Key, or the admin API:

POST /api/v1/admin/developer/partners/{partnerId}/keys
Authorization: Bearer <admin-jwt>
Content-Type: application/json

{
  "name": "Production Key",
  "scopes": ["user:create", "user:read", "auth:code", "balance:read"]
}

Response (shown only once — store securely):

{
  "success": true,
  "data": {
    "apiKey": { "id": "...", "name": "Production Key", "keyPrefix": "pk_live_a1b2" },
    "rawKey": "pk_live_YOUR_KEY_HERE",
    "rawSecret": "sk_live_YOUR_SECRET_HERE",
    "warning": "This secret will not be shown again. Store it securely."
  }
}

Authentication

All Developer API requests are authenticated using HMAC-SHA256 request signing.

Request signing

Every request must include four headers, plus Content-Type: application/json when it has a body:

X-API-Key: pk_live_YOUR_KEY_HERE
X-Timestamp: 1708264800000
X-Request-Id: 550e8400-e29b-41d4-a716-446655440000
X-Signature: <hmac-sha256-hex>

Signature construction:

signature = hex(HMAC-SHA256(
  key     = sk_live_YOUR_SECRET_HERE,
  message = "v1" + "\n" + HTTP_METHOD + "\n" + PATH_AND_QUERY + "\n" + TIMESTAMP + "\n" + REQUEST_ID + "\n" + RAW_BODY
))

Where:

  • PATH_AND_QUERY is the full path as sent, starting with /api/v1. Query parameters, if any, are sorted by key then value and URL-encoded.
  • TIMESTAMP is a Unix timestamp in milliseconds, the same string as the header. Requests more than 5 minutes old or more than 30 seconds in the future are rejected.
  • REQUEST_ID is 1-128 characters matching ^[A-Za-z0-9._:-]+$, unique per request. Reuse within 5 minutes returns 409 DUPLICATE_REQUEST.
  • RAW_BODY is the exact request body, not a hash. For requests with no body it is empty, so the message ends with a newline.

Example (Node.js):

const crypto = require('crypto');

// path has no query string here; no signed endpoint reads one
function signRequest(apiKey, apiSecret, method, path, body = '') {
  const timestamp = Date.now().toString();
  const requestId = crypto.randomUUID();
  const message = ['v1', method.toUpperCase(), path, timestamp, requestId, body].join('\n');
  const signature = crypto.createHmac('sha256', apiSecret).update(message).digest('hex');

  return {
    'X-API-Key': apiKey,
    'X-Timestamp': timestamp,
    'X-Request-Id': requestId,
    'X-Signature': signature,
    'Content-Type': 'application/json',
  };
}

const body = JSON.stringify({ externalUserId: 'user_12345', username: 'john_doe' });
const headers = signRequest(KEY, SECRET, 'POST', '/api/v1/developer/users', body);

Send exactly the body string you signed. See the Authentication guide for Python, PHP and cURL samples and a test vector.

IP allowlist

API keys can be restricted to specific IP addresses. Requests from unlisted IPs return 403 IP_NOT_ALLOWED. Configure via:

PUT /api/v1/admin/developer/keys/{keyId}
{ "ipAllowlist": ["203.0.113.1", "198.51.100.0/24"] }

API Scopes

ScopeAllows
user:createCreate player accounts linked to this partner and update their profiles
user:readRead player information by external ID
auth:codeGenerate one-time codes for SSO login, auto-provisioning new players
balance:readRead player balance
balance:writeCredit or debit player balance
webhooks:manageSubscribe to / manage webhook endpoints

User Management

Create a user

POST /api/v1/developer/users

Maps an external user ID to a poker platform account. If the external ID already has a mapping, returns the existing user unchanged with isNew: false and status 200 (a new user is 201).

{
  "externalUserId": "user_12345",
  "username": "john_doe",
  "email": "john@example.com",
  "firstName": "John",
  "lastName": "Doe"
}

Only externalUserId is always required. country, avatar and metadata are also accepted.

Response:

{
  "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-20T12:00:00.000Z"
  },
  "isNew": true
}

Username policy (autoProvisionUsername, configured per partner, default generate_from_email):

  • require — username must be provided.
  • generate_from_email — use username if provided, otherwise derive it from email. One of them must be provided.
  • generate_random — use username if provided, otherwise generate a random player_ username.

Update a user

PUT /api/v1/developer/users/{externalUserId}

Requires user:create. Accepts firstName, lastName, country and avatar. Username and email cannot be changed.

Read a user

GET /api/v1/developer/users/{externalUserId}

Returns the same user object as above. Requires user:read.

SSO Authentication (Auth Codes)

To log a user into the poker platform from your platform without sharing passwords:

Step 1: Generate an auth code

POST /api/v1/developer/auth/code

Requires auth:code scope.

{
  "externalUserId": "user_12345",
  "mode": "cookie"
}

mode is cookie (default) or token. If the user has no mapping yet, one is auto-provisioned, using optional username, email, firstName and lastName.

Response (201):

{
  "code": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456",
  "expiresIn": 60,
  "mode": "cookie"
}

Auth codes are 64 hex characters, expire after 60 seconds and are single-use.

Step 2: Exchange the code for a session

The player’s browser sends the code to the public exchange endpoint:

POST /api/v1/auth/exchange
Content-Type: application/json

{ "code": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456" }

In cookie mode the response sets SameSite=Strict HttpOnly session cookies and returns { success, data: { user, expiresIn } }. In token mode it returns { success, data: { accessToken, refreshToken, expiresIn, user } } and sets no cookies. See the SSO guide.

Balance Operations

Requires balance:read or balance:write scope.

Read balance

GET /api/v1/developer/users/{externalUserId}/balance

{
  "balance": "1250",
  "virtualBalance": "0",
  "tournamentTickets": "0"
}

Values are strings without trailing zeros.

Credit balance

POST /api/v1/developer/users/{externalUserId}/balance/credit

{
  "amount": "100.00",
  "description": "Deposit from external platform",
  "reference": "ext_txn_99"
}

reference is an optional idempotency key, scoped to your partner. Repeating a request with the same reference returns the original transaction with isNew: false and status 200 (a new transaction is 201). Reusing it for a different user, amount or operation returns 409 IDEMPOTENCY_CONFLICT. amount is a string with at most 2 decimal places.

Response:

{
  "transaction": {
    "id": "f6e5d4c3-b2a1-0987-fedc-ba0987654321",
    "type": "DEPOSIT",
    "amount": "100",
    "balanceBefore": "1250",
    "balanceAfter": "1350",
    "reference": "ext_txn_99",
    "description": "Deposit from external platform",
    "createdAt": "2026-02-20T12:00:00.000Z"
  },
  "isNew": true
}

Debit balance

POST /api/v1/developer/users/{externalUserId}/balance/debit

{
  "amount": "50.00",
  "description": "Withdrawal to external platform",
  "reference": "ext_wd_77"
}

The response has the same shape, with type WITHDRAWAL. Debit fails with 400 INSUFFICIENT_BALANCE if the user’s balance is insufficient. Credits and debits return 403 ACCOUNT_BLOCKED or 403 ACCOUNT_FROZEN for those accounts.

Webhooks

Subscribing to events

POST /api/v1/developer/webhooks

Requires webhooks:manage scope.

{
  "eventType": "deposit.confirmed",
  "url": "https://myplatform.com/poker-webhooks"
}

If a subscription already exists for this eventType, it is updated (URL + reactivated).

Supported event types

EventFired when
user.createdA player account is created via the API, including auto-provisioning
user.updatedA player’s profile is updated
deposit.confirmedA deposit is confirmed and credited
deposit.failedA deposit fails
withdrawal.completedA withdrawal completes
withdrawal.failedA withdrawal fails
balance.creditedBalance is credited via the Developer API
balance.debitedBalance is debited via the Developer API

Webhook payload format

{
  "event": "deposit.confirmed",
  "data": {
    "userId": "cuid_internal",
    "amount": "100",
    "transactionId": "txn_id",
    "currency": "USD"
  },
  "timestamp": "2026-02-20T12:00:00.000Z",
  "webhookId": "uuid-v4",
  "partnerId": "partner_id"
}

Signature verification

Deliveries are signed only if a webhook secret has been generated for your partner. There is none by default, and the admin panel has no control for it, so an admin generates it through the API. The secret (whsec_...) is returned once:

POST /api/v1/admin/developer/partners/{partnerId}/webhook-secret
Authorization: Bearer <admin-jwt>

Signed deliveries include a plain hex X-Signature header, with no prefix:

X-Signature: <hmac-hex>

Verify in your receiver:

const crypto = require('crypto');

function verifyWebhook(secret, rawBody, signature) {
  if (!signature) return false;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(signature, 'hex');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Always use timingSafeEqual to prevent timing attacks.

Delivery reliability

  • 5 delivery attempts in total: the first, then retries after 30s → 2m → 10m → 1h.
  • A delivery is considered failed after the fifth attempt.
  • Delivery times out after 10 seconds, redirects are not followed, and any non-2xx response counts as a failure.
  • Delivery history is at Admin → Developer API → Webhook Deliveries.
  • The retry processor runs every 30 seconds.

Managing subscriptions

GET    /api/v1/developer/webhooks        — list all subscriptions with stats
DELETE /api/v1/developer/webhooks/{id}   — unsubscribe

Partner Management (Admin API)

These routes require admin authentication and are not accessible with partner API keys.

Partner lifecycle

ActionEndpoint
List partnersGET /api/v1/admin/developer/partners
Get partnerGET /api/v1/admin/developer/partners/{id}
Create partnerPOST /api/v1/admin/developer/partners
Update partnerPUT /api/v1/admin/developer/partners/{id}
Approve partnerPOST /api/v1/admin/developer/partners/{id}/approve
Reject partner (deletes the record)POST /api/v1/admin/developer/partners/{id}/reject
Suspend partnerPOST /api/v1/admin/developer/partners/{id}/suspend
Reactivate partnerPOST /api/v1/admin/developer/partners/{id}/reactivate
Delete partnerDELETE /api/v1/admin/developer/partners/{id}?confirm=true
Generate webhook secretPOST /api/v1/admin/developer/partners/{id}/webhook-secret

API key management

ActionEndpoint
List keysGET /api/v1/admin/developer/partners/{partnerId}/keys
Generate keyPOST /api/v1/admin/developer/partners/{partnerId}/keys
Update keyPUT /api/v1/admin/developer/keys/{keyId}
Rotate secretPOST /api/v1/admin/developer/keys/{keyId}/rotate
Revoke keyDELETE /api/v1/admin/developer/keys/{keyId}

Key rotation generates a new secret while keeping the old one valid for a grace period (gracePeriodHours, default 24, from 0 to 720). After the grace period, only the new secret works.

Audit log

Partner API calls that pass authentication are logged. Query via:

GET /api/v1/admin/developer/audit-log?partnerId={id}&action={action}&startDate={iso}&endDate={iso}

Webhook deliveries

ActionEndpoint
List deliveriesGET /api/v1/admin/developer/webhooks/deliveries?partnerId={id}&eventType={type}&status={delivered|failed|pending}
Retry a deliveryPOST /api/v1/admin/developer/webhooks/deliveries/{id}/retry
Aggregate statsGET /api/v1/admin/developer/webhooks/stats

External User Mappings

Each newly provisioned user gets an ExternalUserMapping record linking the partner’s externalUserId to the internal userId. This mapping is used to:

  • Route deposit.* and withdrawal.* webhook events only to partners mapped to that user.
  • Prevent the same external user from being mapped to two different internal users within a partner.
  • Allow the admin to audit which users came from which partner.

View mappings via:

GET /api/v1/admin/developer/partners/{partnerId}/users

Developer Settings

Global settings for the Developer API:

SettingDefaultDescription
selfRegistrationEnabledfalseAllow partners to self-register
requireAdminApprovaltrueNew registrations need admin approval
defaultScopes[]Scopes for keys created when a partner is auto-approved or approved with generateKey
defaultRateLimitPerMinute100Rate limit for those same keys
usernamePolicyrequireStored username policy. Partners carry their own autoProvisionUsername, which is what provisioning applies
allowTokenModetrueAllow token mode SSO codes
allowCookieModetrueAllow cookie mode SSO codes

Update via:

PUT /api/v1/admin/developer/settings
Authorization: Bearer <admin-jwt>

{
  "defaultRateLimitPerMinute": 120,
  "selfRegistrationEnabled": true
}

Error Responses

Errors from partner endpoints use this format:

{
  "error": "ERROR_CODE",
  "message": "Human-readable description"
}

POST /api/v1/auth/exchange, body validation (VALIDATION_ERROR) and feature-toggle errors use a nested format instead:

{
  "success": false,
  "error": { "code": "ERROR_CODE", "message": "Human-readable description" }
}

See the Error Reference for every code.

Rate Limiting

Rate limits are enforced per API key, over a one-minute window. The default is 100 requests/minute. Custom limits (1 to 10000) can be set per key:

{ "rateLimitPerMinute": 300 }

Exceeded limits return 429 RATE_LIMIT_EXCEEDED with a Retry-After header. Every request made with a valid key counts, including rejected ones.

Key Files Reference

FilePurpose
src/api/routes/developer/Partner-facing API routes
src/api/routes/auth.routes.tsPOST /api/v1/auth/exchange code exchange
src/api/routes/admin/developer.routes.tsAdmin management routes
src/services/developer-api-key.service.tsKey generation, verification, rotation
src/services/developer-partner.service.tsPartner CRUD and lifecycle
src/services/developer-user.service.tsUser provisioning and auth codes
src/services/developer-balance.service.tsBalance credit/debit operations
src/services/developer-webhook.service.tsSubscription management + dispatch engine
src/api/middleware/developer-auth.tsRequest signing verification
Try the live demo Pricing

Have questions about Poker Engine? We reply fast.