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_QUERYis the full path as sent, starting with/api/v1. Query parameters, if any, are sorted by key then value and URL-encoded.TIMESTAMPis 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_IDis 1-128 characters matching^[A-Za-z0-9._:-]+$, unique per request. Reuse within 5 minutes returns409 DUPLICATE_REQUEST.RAW_BODYis 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
| Scope | Allows |
|---|---|
user:create | Create player accounts linked to this partner and update their profiles |
user:read | Read player information by external ID |
auth:code | Generate one-time codes for SSO login, auto-provisioning new players |
balance:read | Read player balance |
balance:write | Credit or debit player balance |
webhooks:manage | Subscribe 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—usernamemust be provided.generate_from_email— useusernameif provided, otherwise derive it fromemail. One of them must be provided.generate_random— useusernameif provided, otherwise generate a randomplayer_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
| Event | Fired when |
|---|---|
user.created | A player account is created via the API, including auto-provisioning |
user.updated | A player’s profile is updated |
deposit.confirmed | A deposit is confirmed and credited |
deposit.failed | A deposit fails |
withdrawal.completed | A withdrawal completes |
withdrawal.failed | A withdrawal fails |
balance.credited | Balance is credited via the Developer API |
balance.debited | Balance 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
| Action | Endpoint |
|---|---|
| List partners | GET /api/v1/admin/developer/partners |
| Get partner | GET /api/v1/admin/developer/partners/{id} |
| Create partner | POST /api/v1/admin/developer/partners |
| Update partner | PUT /api/v1/admin/developer/partners/{id} |
| Approve partner | POST /api/v1/admin/developer/partners/{id}/approve |
| Reject partner (deletes the record) | POST /api/v1/admin/developer/partners/{id}/reject |
| Suspend partner | POST /api/v1/admin/developer/partners/{id}/suspend |
| Reactivate partner | POST /api/v1/admin/developer/partners/{id}/reactivate |
| Delete partner | DELETE /api/v1/admin/developer/partners/{id}?confirm=true |
| Generate webhook secret | POST /api/v1/admin/developer/partners/{id}/webhook-secret |
API key management
| Action | Endpoint |
|---|---|
| List keys | GET /api/v1/admin/developer/partners/{partnerId}/keys |
| Generate key | POST /api/v1/admin/developer/partners/{partnerId}/keys |
| Update key | PUT /api/v1/admin/developer/keys/{keyId} |
| Rotate secret | POST /api/v1/admin/developer/keys/{keyId}/rotate |
| Revoke key | DELETE /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
| Action | Endpoint |
|---|---|
| List deliveries | GET /api/v1/admin/developer/webhooks/deliveries?partnerId={id}&eventType={type}&status={delivered|failed|pending} |
| Retry a delivery | POST /api/v1/admin/developer/webhooks/deliveries/{id}/retry |
| Aggregate stats | GET /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.*andwithdrawal.*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:
| Setting | Default | Description |
|---|---|---|
selfRegistrationEnabled | false | Allow partners to self-register |
requireAdminApproval | true | New registrations need admin approval |
defaultScopes | [] | Scopes for keys created when a partner is auto-approved or approved with generateKey |
defaultRateLimitPerMinute | 100 | Rate limit for those same keys |
usernamePolicy | require | Stored username policy. Partners carry their own autoProvisionUsername, which is what provisioning applies |
allowTokenMode | true | Allow token mode SSO codes |
allowCookieMode | true | Allow 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
| File | Purpose |
|---|---|
src/api/routes/developer/ | Partner-facing API routes |
src/api/routes/auth.routes.ts | POST /api/v1/auth/exchange code exchange |
src/api/routes/admin/developer.routes.ts | Admin management routes |
src/services/developer-api-key.service.ts | Key generation, verification, rotation |
src/services/developer-partner.service.ts | Partner CRUD and lifecycle |
src/services/developer-user.service.ts | User provisioning and auth codes |
src/services/developer-balance.service.ts | Balance credit/debit operations |
src/services/developer-webhook.service.ts | Subscription management + dispatch engine |
src/api/middleware/developer-auth.ts | Request signing verification |