Casino Api Pro

Get started

Idempotency

Why every money endpoint is idempotent on your transaction_id, and how to write a retry loop that cannot double-debit.

Every endpoint that moves money is idempotent on an identifier you supply. Send the same transaction_id twice and the money moves once. This is the single most important contract in the API, and the one worth testing hardest.

How it works

A transaction is claimed by a unique database index on (operator_id, transaction_id). The first request to arrive wins the claim and proceeds to your wallet. Any later request carrying the same id finds the existing record and returns its stored result — it never reaches your wallet a second time.

  • The guarantee is enforced by the database, not by application code, so it holds under concurrency.
  • It holds across processes and across restarts.
  • It is scoped to your operator account — your ids cannot collide with another operator's.

Choosing an id

Mint it yourself, make it unique, and make it derivable — you must be able to produce the same id again when you retry. A UUID stored alongside the bet in your own database is the usual answer.

js
// Good: generated once, persisted with the bet, reused on every retry.
const transactionId = bet.id;             // your own primary key
await placeBet({ transaction_id: transactionId, amount: "10.00" });

// Bad: a fresh id per attempt turns one retry into two debits.
await placeBet({ transaction_id: crypto.randomUUID(), amount: "10.00" });

What a replay returns

A replayed request returns the original outcome with 200 OK — the same balance, the same round, the same everything. It is indistinguishable from the first call except that nothing moved.

json
{
  "transaction_id": "bet_9f21c4",
  "status": "COMPLETED",
  "balance": "489.50",
  "currency": "GHS",
  "replayed": true
}

Same id, different body

Reusing an id with a different amount, player or currency is a bug on your side, and we treat it as one: the request is rejected with 409 IDEMPOTENCY_CONFLICT and nothing moves. We will not guess which of the two you meant.

Timeouts are the whole point

!A 504 is not a failure

If a call to your wallet times out, we do not know whether it applied. We leave the transaction PENDING and answer 504 WALLET_TIMEOUT, which is marked retryable. Retry with the same id. The retry resolves the pending record — it does not create a second one.

This is why a fresh id per attempt is dangerous: it converts a recoverable ambiguity into a double debit that only reconciliation will find.

js
async function settle(bet) {
  for (let attempt = 0; attempt < 5; attempt++) {
    try {
      // Same id every time — that is what makes this safe.
      return await api.post("/v1/transactions/bet", {
        transaction_id: bet.id,
        round_id: bet.roundId,
        player_id: bet.playerId,
        amount: bet.amount,
        currency: bet.currency,
      });
    } catch (err) {
      if (!err.retryable) throw err;
      await sleep(2 ** attempt * 250);
    }
  }
  throw new Error("wallet did not settle; leave for reconciliation");
}

What is not idempotent

Reads are naturally safe to repeat. Session creation is not idempotent — calling it twice mints two sessions, which is harmless but wasteful. See Sessions.