Skip to main content
All Seamless Wallet calls include a unique request_id. Network timeouts, gateway hiccups, or internal PlayStarters retries can cause the same request to arrive at your endpoint more than once. Your implementation must handle this without re-applying the financial movement.

The rule

If you receive a request with a request_id you have already processed, do not reapply the movement. Return HTTP 200 OK with the player’s current balance.
1

Store processed request_ids

On every successful BET, WIN, or VOID, persist the request_id together with the resulting balance in a transactions table or cache. Use a unique index/constraint on request_id to prevent duplicates at the database level.
2

Check before applying

Before applying a new transaction, look up the request_id. If it already exists, skip the movement and return the previously stored balance.
3

Wrap the read + write in a single transaction

The check, the movement, and the request_id write must be atomic to avoid race conditions when the same request arrives twice in parallel.

Example pseudocode

async function handleWalletCallback(req) {
  return db.transaction(async (tx) => {
    const existing = await tx.transactions.find(req.request_id);
    if (existing) {
      return { balance: existing.balance_after };
    }

    if (req.type === "BALANCE") {
      const balance = await tx.players.getBalance(req.player_id);
      return { balance };
    }

    // BET / WIN / VOID
    const balance = await tx.players.applyMovement(req);
    await tx.transactions.insert({
      request_id: req.request_id,
      player_id: req.player_id,
      type: req.type,
      amount: req.amount,
      balance_after: balance,
    });
    return { balance };
  });
}
Idempotency is mandatory. Without it, a single retried BET can debit the player twice, or a retried WIN can credit them twice — both create reconciliation issues that are painful to unwind.