Phoenix Games
Webhook Endpoints

Batch Deposit

Credit multiple player balances in a single request for high-volume games

POST {api_base_url}/deposit/batch

Called to credit multiple players' balances in a single request — used for high-volume games like Keno where many bets close simultaneously.

This endpoint is optional and used for performance optimization. If not implemented, Phoenix Games will continue using individual /deposit calls for each bet.

When to Use

Batch deposit is designed for games where many bets (hundreds to thousands) close at the same time, such as:

  • Keno - All tickets close when the round ends (can be 1000+ bets)
  • Bingo - All cards resolve simultaneously
  • Crash - All active bets cash out when the game crashes

Benefits:

  • Reduces HTTP request overhead from thousands of individual calls to ~10 batch requests
  • Lower network latency and improved throughput
  • Easier database transaction management with batch updates

Enabling Batch Deposit

To enable batch deposit for your operator integration:

  1. Implement the /deposit/batch endpoint
  2. Contact Phoenix Games support to enable the supports_batch_deposit flag for your operator

Once enabled, Phoenix Games will automatically route bet deposits to the batch endpoint instead of individual calls.

Request Fields

FieldTypeDescription
betsarray of BetDepositItemList of bets to credit

BetDepositItem Structure

FieldTypeDescription
player_idstringUnique player ID
bet_idstringUnique bet ID
amountintegerTotal amount being deposited (in cents)
gamestringGame ID
instance_idstringGame instance ID
round_idstringID of the round
wagerintegerAmount wagered (in cents)
wonintegerAmount won (in cents)
tx_idstringUnique transaction ID

Request Example

{
  "bets": [
    {
      "player_id": "player_123",
      "bet_id": "bet_abc",
      "amount": 15000,
      "game": "keno",
      "instance_id": "instance_xyz",
      "round_id": "round_789",
      "wager": 10000,
      "won": 15000,
      "tx_id": "deposit:bet:bet_abc"
    },
    {
      "player_id": "player_456",
      "bet_id": "bet_def",
      "amount": 5000,
      "game": "keno",
      "instance_id": "instance_xyz",
      "round_id": "round_789",
      "wager": 10000,
      "won": 5000,
      "tx_id": "deposit:bet:bet_def"
    }
    // ... up to 1000 bets per batch
  ]
}

Response Format

Success Response

Return the updated balance for each unique player in the batch:

{
  "type": "SUCCESS",
  "balances": [
    {
      "player_id": "player_123",
      "balance": 25000.50
    },
    {
      "player_id": "player_456",
      "balance": 12500.75
    }
  ],
  "timestamp": 1712401234567
}
FieldTypeDescription
typestringAlways "SUCCESS" for successful operations
balancesarray of PlayerBalanceUpdated balances for each player
timestampintegerTimestamp in milliseconds

PlayerBalance Structure

FieldTypeDescription
player_idstringPlayer ID
balancefloatUpdated player balance after all deposits

Error Response

If any bet in the batch fails validation, reject the entire batch:

{
  "type": "ERROR",
  "code": "BATCH_VALIDATION_FAILED"
}
FieldTypeDescription
typestringAlways "ERROR" for failed operations
codestringError code explaining failure reason

Error Codes

  • BATCH_VALIDATION_FAILED - One or more bets in the batch failed validation
  • INVALID_REQUEST - Request format is invalid
  • PLAYER_NOT_FOUND - One or more players don't exist
  • INSUFFICIENT_BALANCE - Internal error (shouldn't happen for deposits)

Implementation Guidelines

1. Transaction Handling

Process the entire batch in a single database transaction for atomicity:

async function processBatchDeposit(request: BatchDepositRequest) {
  return await db.transaction(async (tx) => {
    // Process all bets in the batch
    for (const bet of request.bets) {
      // Check idempotency
      if (await alreadyProcessed(tx, bet.tx_id)) {
        continue;
      }

      // Credit player balance
      await tx.execute(
        'UPDATE players SET balance = balance + ? WHERE id = ?',
        [bet.amount / 100, bet.player_id]
      );

      // Mark transaction as processed
      await tx.execute(
        'INSERT INTO processed_transactions (tx_id, bet_id) VALUES (?, ?)',
        [bet.tx_id, bet.bet_id]
      );
    }

    // Fetch updated balances for all unique players
    const playerIds = [...new Set(request.bets.map(b => b.player_id))];
    const balances = await tx.query(
      'SELECT id, balance FROM players WHERE id IN (?)',
      [playerIds]
    );

    return {
      type: 'SUCCESS',
      balances: balances.map(p => ({
        player_id: p.id,
        balance: p.balance
      })),
      timestamp: Date.now()
    };
  });
}

2. Idempotency

CRITICAL: Check each tx_id for idempotency to prevent duplicate credits:

async function alreadyProcessed(tx, txId: string): Promise<boolean> {
  const result = await tx.query(
    'SELECT 1 FROM processed_transactions WHERE tx_id = ? LIMIT 1',
    [txId]
  );
  return result.length > 0;
}

3. Validation

Validate the entire batch before processing any bets:

  • Verify all player_ids exist
  • Check all tx_ids are unique within the batch
  • Ensure amounts are positive
  • Validate round_id matches across all bets (they're from the same round)

If validation fails, return an error without processing any bets.

4. Performance Optimization

For large batches (500-1000 bets):

  • Use batch INSERT/UPDATE queries instead of individual queries
  • Consider using prepared statements with batch execution
  • Use indexes on tx_id and player_id columns
  • Minimize database round trips

Example batch update:

-- Batch update balances using VALUES clause
UPDATE players SET balance = balance + updates.amount
FROM (VALUES
  ('player_123', 150.00),
  ('player_456', 50.00)
) AS updates(player_id, amount)
WHERE players.id = updates.player_id;

Batch Behavior

Batch Sizing

Phoenix Games sends batches based on:

  • Max size: 1000 bets per batch
  • Max wait: 50ms from first bet in the batch

This means:

  • If 1000 bets close simultaneously → 1 batch of 1000 bets
  • If 8000 bets close simultaneously → 8 batches of 1000 bets each
  • If only 200 bets close → 1 batch of 200 bets after 50ms

Retry Behavior

If a batch deposit fails:

  • Phoenix Games retries the entire batch up to 10 times with exponential backoff
  • Same retry logic as individual /deposit calls
  • All bets in the batch will be NACK'd and redelivered if max retries exceeded

Security

Batch requests are signed the same way as individual requests:

  • Request body is signed with RSA SHA256
  • Signature included in signature header
  • Verify signature covers the entire batch body

Migration Guide

Step 1: Implement the Endpoint

Add the /deposit/batch endpoint to your API while keeping the existing /deposit endpoint.

Step 2: Test Thoroughly

Test with various batch sizes:

  • Small batches (1-10 bets)
  • Medium batches (100-500 bets)
  • Large batches (1000 bets)
  • Ensure idempotency works correctly
  • Verify transaction rollback on errors

Step 3: Enable in Production

Contact Phoenix Games support to enable the supports_batch_deposit flag. We'll enable it for your operator after reviewing your implementation.

Step 4: Monitor

After enabling:

  • Monitor batch processing times
  • Check for any failed batches in logs
  • Verify player balance accuracy
  • Compare batch vs individual endpoint performance

Backward Compatibility

If batch deposit is not implemented or disabled:

  • Phoenix Games automatically falls back to individual /deposit calls
  • No configuration changes needed
  • Existing integrations continue to work unchanged

All-or-Nothing Processing

If the batch deposit endpoint returns an error, the entire batch is rejected and retried. Ensure your implementation processes all bets atomically — either all succeed or all fail.

Example Implementations

import express from 'express';
import { verifySignature } from './security';

app.post('/deposit/batch', async (req, res) => {
  // Verify signature
  if (!verifySignature(req.body, req.headers.signature)) {
    return res.status(401).json({ type: 'ERROR', code: 'INVALID_SIGNATURE' });
  }

  const { bets } = req.body;

  try {
    // Process in transaction
    const result = await db.transaction(async (tx) => {
      const playerBalances = new Map();

      for (const bet of bets) {
        // Check idempotency
        const exists = await tx.query(
          'SELECT 1 FROM transactions WHERE tx_id = ?',
          [bet.tx_id]
        );

        if (exists.length > 0) continue;

        // Update balance
        await tx.execute(
          'UPDATE players SET balance = balance + ? WHERE id = ?',
          [bet.amount / 100, bet.player_id]
        );

        // Record transaction
        await tx.execute(
          'INSERT INTO transactions (tx_id, bet_id, player_id, amount) VALUES (?, ?, ?, ?)',
          [bet.tx_id, bet.bet_id, bet.player_id, bet.amount]
        );

        playerBalances.set(bet.player_id, null); // Mark for balance fetch
      }

      // Fetch updated balances
      const playerIds = Array.from(playerBalances.keys());
      const balances = await tx.query(
        'SELECT id, balance FROM players WHERE id IN (?)',
        [playerIds]
      );

      return balances.map(p => ({
        player_id: p.id,
        balance: p.balance
      }));
    });

    res.json({
      type: 'SUCCESS',
      balances: result,
      timestamp: Date.now()
    });
  } catch (error) {
    console.error('Batch deposit error:', error);
    res.status(500).json({
      type: 'ERROR',
      code: 'BATCH_PROCESSING_FAILED'
    });
  }
});
from flask import Flask, request, jsonify
from sqlalchemy import create_engine
from contextlib import contextmanager

@app.route('/deposit/batch', methods=['POST'])
def batch_deposit():
    # Verify signature
    if not verify_signature(request.get_json(), request.headers.get('signature')):
        return jsonify({'type': 'ERROR', 'code': 'INVALID_SIGNATURE'}), 401

    data = request.get_json()
    bets = data['bets']

    try:
        with transaction() as tx:
            player_ids = set()

            for bet in bets:
                # Check idempotency
                if tx.execute(
                    "SELECT 1 FROM transactions WHERE tx_id = %s",
                    (bet['tx_id'],)
                ).fetchone():
                    continue

                # Update balance
                tx.execute(
                    "UPDATE players SET balance = balance + %s WHERE id = %s",
                    (bet['amount'] / 100, bet['player_id'])
                )

                # Record transaction
                tx.execute(
                    "INSERT INTO transactions (tx_id, bet_id, player_id, amount) VALUES (%s, %s, %s, %s)",
                    (bet['tx_id'], bet['bet_id'], bet['player_id'], bet['amount'])
                )

                player_ids.add(bet['player_id'])

            # Fetch updated balances
            balances = tx.execute(
                "SELECT id, balance FROM players WHERE id = ANY(%s)",
                (list(player_ids),)
            ).fetchall()

            return jsonify({
                'type': 'SUCCESS',
                'balances': [
                    {'player_id': row[0], 'balance': float(row[1])}
                    for row in balances
                ],
                'timestamp': int(time.time() * 1000)
            })

    except Exception as e:
        print(f"Batch deposit error: {e}")
        return jsonify({
            'type': 'ERROR',
            'code': 'BATCH_PROCESSING_FAILED'
        }), 500
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

Route::post('/deposit/batch', function (Request $request) {
    // Verify signature
    if (!verifySignature($request->all(), $request->header('signature'))) {
        return response()->json([
            'type' => 'ERROR',
            'code' => 'INVALID_SIGNATURE'
        ], 401);
    }

    $bets = $request->input('bets');

    try {
        $balances = DB::transaction(function () use ($bets) {
            $playerIds = [];

            foreach ($bets as $bet) {
                // Check idempotency
                $exists = DB::table('transactions')
                    ->where('tx_id', $bet['tx_id'])
                    ->exists();

                if ($exists) continue;

                // Update balance
                DB::table('players')
                    ->where('id', $bet['player_id'])
                    ->increment('balance', $bet['amount'] / 100);

                // Record transaction
                DB::table('transactions')->insert([
                    'tx_id' => $bet['tx_id'],
                    'bet_id' => $bet['bet_id'],
                    'player_id' => $bet['player_id'],
                    'amount' => $bet['amount']
                ]);

                $playerIds[] = $bet['player_id'];
            }

            // Fetch updated balances
            $playerIds = array_unique($playerIds);
            return DB::table('players')
                ->whereIn('id', $playerIds)
                ->get(['id', 'balance'])
                ->map(function ($player) {
                    return [
                        'player_id' => $player->id,
                        'balance' => (float) $player->balance
                    ];
                })->toArray();
        });

        return response()->json([
            'type' => 'SUCCESS',
            'balances' => $balances,
            'timestamp' => now()->timestamp * 1000
        ]);

    } catch (\Exception $e) {
        Log::error('Batch deposit error: ' . $e->getMessage());
        return response()->json([
            'type' => 'ERROR',
            'code' => 'BATCH_PROCESSING_FAILED'
        ], 500);
    }
});
?>

Testing Recommendations

  1. Idempotency Test: Send the same batch twice, verify balances only credited once
  2. Partial Duplicate Test: Include some already-processed tx_ids in a batch
  3. Multiple Players Test: Batch with bets from different players
  4. Single Player Test: All bets in batch from one player (ensure correct final balance)
  5. Error Recovery Test: Force a database error mid-batch, verify rollback
  6. Large Batch Test: Send 1000 bets, verify all processed within reasonable time
  7. Signature Validation Test: Send batch with invalid signature, verify rejection

Troubleshooting

High Latency

If batch processing is slow:

  • Use batch UPDATE queries instead of individual updates
  • Add database indexes on tx_id and player_id
  • Consider connection pooling
  • Profile your database queries

Memory Issues

For very large batches:

  • Process in smaller chunks within the transaction
  • Stream balance fetches instead of loading all at once
  • Limit batch size via configuration

Partial Failures

If some bets fail mid-batch:

  • Ensure entire transaction rolls back
  • Log the specific error for debugging
  • Return generic error to Phoenix Games (batch will retry)