Balance

This guide explains how to read and modify user balances through the Developer API. All balance operations are atomic and support idempotency via reference strings to prevent duplicate transactions.

Required scopes: balance:read (read balance), balance:write (credit and debit)

Reading a Balance

Retrieve a user’s current balance by their external user ID.

const { apiRequest } = require('./hmac-client'); // See quickstart.md

const result = await apiRequest('GET', '/developer/users/user_12345/balance');

console.log('Balance:', result.balance);
console.log('Virtual balance:', result.virtualBalance);
console.log('Tournament tickets:', result.tournamentTickets);

Request:

GET /api/v1/developer/users/user_12345/balance

Response (200 OK):

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

Balance Fields

FieldTypeDescription
balancestringReal-money balance available for cash games and withdrawals
virtualBalancestringPlay-money balance for practice tables
tournamentTicketsstringNumber of tournament tickets held by the user

All values are returned as strings to preserve decimal precision. Trailing zeros are not kept, so a balance of 250.00 is returned as "250" and 25.50 as "25.5". See Decimal Precision below.

Crediting Funds

Add funds to a user’s balance. This creates a DEPOSIT transaction record.

const result = await apiRequest('POST', '/developer/users/user_12345/balance/credit', {
  amount: '100.00',
  reference: 'purchase_abc123',
  description: 'Chip purchase — $100 package',
});

console.log('Transaction ID:', result.transaction.id);
console.log('Previous balance:', result.transaction.balanceBefore);
console.log('New balance:', result.transaction.balanceAfter);
console.log('Is new transaction:', result.isNew);

Request:

POST /api/v1/developer/users/user_12345/balance/credit
{
  "amount": "100.00",
  "reference": "purchase_abc123",
  "description": "Chip purchase — $100 package"
}

Response (201 Created):

{
  "transaction": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "type": "DEPOSIT",
    "amount": "100",
    "balanceBefore": "250",
    "balanceAfter": "350",
    "reference": "purchase_abc123",
    "description": "Chip purchase — $100 package",
    "createdAt": "2026-02-18T14:30:00.000Z"
  },
  "isNew": true
}

Request Body Fields

FieldTypeRequiredDescription
amountstringYesAmount to credit. Must be a positive decimal with at most 2 decimal places (e.g., "100.00", "25.50", "1")
currencystringNoAccepted (max 10 chars) but not used or stored
referencestringNoYour unique transaction reference for idempotency (max 255 chars). See Idempotency
descriptionstringNoHuman-readable description stored with the transaction (max 500 chars). Defaults to "Developer API credit"

Debiting Funds

Subtract funds from a user’s balance. This creates a WITHDRAWAL transaction record.

const result = await apiRequest('POST', '/developer/users/user_12345/balance/debit', {
  amount: '50.00',
  reference: 'cashout_xyz789',
  description: 'Cashout to player wallet',
});

console.log('Transaction ID:', result.transaction.id);
console.log('Previous balance:', result.transaction.balanceBefore);
console.log('New balance:', result.transaction.balanceAfter);

Request:

POST /api/v1/developer/users/user_12345/balance/debit
{
  "amount": "50.00",
  "reference": "cashout_xyz789",
  "description": "Cashout to player wallet"
}

Response (201 Created):

{
  "transaction": {
    "id": "f6e5d4c3-b2a1-0987-fedc-ba0987654321",
    "type": "WITHDRAWAL",
    "amount": "50",
    "balanceBefore": "350",
    "balanceAfter": "300",
    "reference": "cashout_xyz789",
    "description": "Cashout to player wallet",
    "createdAt": "2026-02-18T14:35:00.000Z"
  },
  "isNew": true
}

Insufficient Balance

If the user does not have enough funds, the API returns a 400 INSUFFICIENT_BALANCE error:

{
  "error": "INSUFFICIENT_BALANCE",
  "message": "Insufficient balance. Current: 50, requested: 100"
}

If the account is blocked or frozen, credits and debits are refused with 403 ACCOUNT_BLOCKED or 403 ACCOUNT_FROZEN.

Always check the balance before debiting if you want to provide a better user experience. However, the API guarantees atomicity — even under concurrent requests, a debit will never overdraw the account.

Idempotency

Credit and debit operations support idempotent requests via the reference field. If you repeat a request with the same reference, the API returns the original transaction instead of creating a duplicate.

This is separate from X-Request-Id, which must be new on every attempt. Retry with a new request ID and the same reference.

Why Idempotency Matters

Network issues, timeouts, and retries can cause the same request to be sent multiple times. Without idempotency, this could result in a user being credited or debited twice. The reference field prevents this:

// First request — creates the transaction
const first = await apiRequest('POST', '/developer/users/user_12345/balance/credit', {
  amount: '100.00',
  reference: 'purchase_abc123', // Your unique reference
});
console.log(first.isNew); // true
console.log(first.transaction.id); // "txn-abc..."

// Retry (same reference) — returns the existing transaction
const retry = await apiRequest('POST', '/developer/users/user_12345/balance/credit', {
  amount: '100.00',
  reference: 'purchase_abc123', // Same reference
});
console.log(retry.isNew); // false
console.log(retry.transaction.id); // "txn-abc..." (same as first)

How It Works

  • The reference is combined with your partner ID to form a unique key: devapi:{partnerId}:{reference}.
  • When a matching transaction is found, the API returns it with isNew: false and HTTP status 200 (instead of 201).
  • The reference is scoped to your partner — two different partners can use the same reference string without conflict.
  • A reference can be used for only one operation. Reusing it for a different user, a different amount, or the other operation (a credit reference on a debit) returns 409 IDEMPOTENCY_CONFLICT and changes nothing.
  • A replay returns the original transaction, including its original balances and description, and sends no webhook.

Best Practices for References

  1. Use deterministic references — derive them from your business logic (e.g., purchase_${orderId}, bonus_${campaignId}_${userId}).
  2. Include enough context — the reference should uniquely identify the business operation, not just be a random string.
  3. Keep references short — maximum 255 characters.
  4. Always use references for financial operations — this protects against accidental double-credits.
// Good references:
'purchase_order_12345'          // Tied to a specific order
'welcome_bonus_user_42'         // One-time bonus per user
'tournament_rebuy_t100_u42_r2'  // Tournament ID, user, rebuy #2
'monthly_rakeback_2026_02_u42'  // Monthly period + user

// Bad references:
crypto.randomUUID()             // Random — no protection against retries
'credit'                        // Too generic — blocks future credits

Decimal Precision

All balance amounts in the API are represented as strings to avoid floating-point precision issues.

Why Strings?

JavaScript’s number type uses IEEE 754 double-precision floating-point, which cannot represent all decimal values exactly:

// Floating-point precision problem
console.log(0.1 + 0.2); // 0.30000000000000004

// The API avoids this by using strings
console.log("0.10" + "0.20"); // Not what we want either

// Correct: use a decimal library or parse carefully
const balance = "100.50"; // This is exact

Handling Amounts

Sending amounts: Always send amounts as strings with at most 2 decimal places:

// Correct
{ amount: "100.00" }
{ amount: "25.50" }
{ amount: "1" }      // Also valid

// Invalid — will be rejected
{ amount: "100.005" } // More than 2 decimal places
{ amount: "-50.00" }  // Negative
{ amount: "0.00" }    // Zero
{ amount: "abc" }     // Not a number
{ amount: 100 }       // Not a string

Receiving amounts: Parse string amounts carefully in your application:

// Node.js
const balance = parseFloat(result.balance); // Simple but has precision limits
// For financial calculations, use a decimal library:
// npm install decimal.js
const { Decimal } = require('decimal.js');
const precise = new Decimal(result.balance);
# Python
from decimal import Decimal
balance = Decimal(result['balance'])  # Precise decimal arithmetic
// PHP
$balance = bcadd($result['balance'], '0', 2);  // BC Math for precision

Amount Validation Rules

The API enforces these rules for credit and debit amounts:

RuleExampleError
Must be a string of digits with an optional 1-2 decimal places"abc", "-5", "1.005", 100VALIDATION_ERROR
Must be greater than zero"0", "0.00"INVALID_AMOUNT

VALIDATION_ERROR uses the nested error format.

Complete Code Examples

Node.js: Full Balance Workflow

const { apiRequest } = require('./hmac-client');

async function balanceWorkflow() {
  const userId = 'demo_user_001';

  // 1. Check initial balance
  console.log('1. Checking balance...');
  const initial = await apiRequest('GET', `/developer/users/${userId}/balance`);
  console.log(`   Balance: $${initial.balance}`);

  // 2. Credit $100 (with idempotency reference)
  console.log('\n2. Crediting $100...');
  const credit = await apiRequest('POST', `/developer/users/${userId}/balance/credit`, {
    amount: '100.00',
    reference: 'demo_credit_001',
    description: 'Demo credit',
  });
  console.log(`   Transaction: ${credit.transaction.id}`);
  console.log(`   Balance: $${credit.transaction.balanceBefore} → $${credit.transaction.balanceAfter}`);

  // 3. Retry the same credit (idempotent — no double credit)
  console.log('\n3. Retrying same credit (idempotent)...');
  const retry = await apiRequest('POST', `/developer/users/${userId}/balance/credit`, {
    amount: '100.00',
    reference: 'demo_credit_001', // Same reference
    description: 'Demo credit',
  });
  console.log(`   Is new? ${retry.isNew}`); // false
  console.log(`   Same transaction? ${retry.transaction.id === credit.transaction.id}`); // true

  // 4. Debit $30
  console.log('\n4. Debiting $30...');
  const debit = await apiRequest('POST', `/developer/users/${userId}/balance/debit`, {
    amount: '30.00',
    reference: 'demo_debit_001',
    description: 'Cashout request',
  });
  console.log(`   Balance: $${debit.transaction.balanceBefore} → $${debit.transaction.balanceAfter}`);

  // 5. Check final balance
  console.log('\n5. Final balance check...');
  const final = await apiRequest('GET', `/developer/users/${userId}/balance`);
  console.log(`   Balance: $${final.balance}`);

  // 6. Try to debit more than available
  console.log('\n6. Attempting overdraft...');
  try {
    await apiRequest('POST', `/developer/users/${userId}/balance/debit`, {
      amount: '999999.00',
      reference: 'demo_overdraft',
    });
  } catch (err) {
    console.log(`   Error: ${err.message}`); // INSUFFICIENT_BALANCE
  }
}

balanceWorkflow().catch(console.error);

Python: Credit with Idempotency

from hmac_client import api_request

user_id = 'py_user_001'

# Credit funds with idempotency reference
result = api_request('POST', f'/developer/users/{user_id}/balance/credit', {
    'amount': '50.00',
    'reference': 'purchase_order_789',
    'description': 'Chip purchase',
})

print(f"Transaction: {result['transaction']['id']}")
print(f"New balance: ${result['transaction']['balanceAfter']}")
print(f"Is new: {result['isNew']}")

# Retry safely — same reference returns same transaction
retry = api_request('POST', f'/developer/users/{user_id}/balance/credit', {
    'amount': '50.00',
    'reference': 'purchase_order_789',
    'description': 'Chip purchase',
})
print(f"Retry is new: {retry['isNew']}")  # False

PHP: Debit with Error Handling

<?php
require_once 'hmac-client.php';

$userId = 'php_user_001';

// Check balance first
$balance = apiRequest('GET', "/developer/users/{$userId}/balance");
echo "Current balance: \${$balance['balance']}\n";

// Attempt to debit
$amount = '25.00';
try {
    $result = apiRequest('POST', "/developer/users/{$userId}/balance/debit", [
        'amount'      => $amount,
        'reference'   => 'cashout_order_789',
        'description' => 'Player cashout',
    ]);

    echo "Debited: \${$amount}\n";
    echo "New balance: \${$result['transaction']['balanceAfter']}\n";
} catch (Exception $e) {
    if (strpos($e->getMessage(), 'INSUFFICIENT_BALANCE') !== false) {
        echo "Cannot debit: insufficient funds\n";
    } else {
        throw $e;
    }
}

Webhooks for Balance Events

When you credit or debit a user’s balance, the poker engine sends webhook events to your subscribed endpoints (if configured):

EventTrigger
balance.creditedAfter a successful credit via the Developer API
balance.debitedAfter a successful debit via the Developer API

These fire only for new transactions, not idempotent replays. amount in the payload is the string you sent, and reference is null if you sent none.

Example balance.credited webhook payload:

{
  "event": "balance.credited",
  "data": {
    "userId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "externalUserId": "user_12345",
    "amount": "100.00",
    "reference": "purchase_abc123",
    "newBalance": "350",
    "partnerId": "partner-uuid"
  },
  "timestamp": "2026-02-18T14:30:00.000Z",
  "webhookId": "550e8400-e29b-41d4-a716-446655440000",
  "partnerId": "partner-uuid"
}

See the Webhooks guide for how to subscribe to events and verify signatures.

Error Reference

Error CodeHTTP StatusCauseResolution
USER_NOT_FOUND404No user mapped to this externalUserIdProvision the user first with POST /developer/users
VALIDATION_ERROR400amount is missing or not a non-negative decimal string with at most 2 decimal places, or another field is too long. Uses the nested error formatFix the fields listed in error.details
INVALID_AMOUNT400Amount is zeroSend an amount greater than zero
INSUFFICIENT_BALANCE400User’s balance is less than the debit amountCheck the balance first, or handle this error gracefully
ACCOUNT_BLOCKED403The user’s account is blocked (credit and debit only)Contact the platform admin
ACCOUNT_FROZEN403The user’s account is frozen (credit and debit only)Contact the platform admin
IDEMPOTENCY_CONFLICT409The reference was already used for a different user, amount, or operationUse a new reference for a new operation

Next Steps

Try the live demo Pricing

Have questions about Poker Engine? We reply fast.