Skip to content

Wallet (transfer) ​

The platform uses a transfer wallet: each player has a game wallet on the platform, and your system moves money in and out through the API. Betting and payouts inside the game are handled in that wallet automatically:

  • The stake is deducted immediately when a bet is accepted (for Niu Niu double bets together with the hold; see the Niu Niu hold).
  • Payouts (stake included; for Niu Niu also the returned hold) are credited automatically on settlement; corrections and voids automatically credit the difference or refund.
  • So the balance is always the available balance, and stakes of unsettled bets can never be withdrawn.
EndpointPurpose
POST /wallet/depositDeposit (your system → game wallet); creates the player if needed
POST /wallet/withdrawWithdraw (game wallet → your system)
GET /wallet/transfer?txnId=Look up the result of a transfer
GET /wallet/balance?username=Current available balance

Deposits and withdrawals ​

json
{ "username": "alice", "txnId": "dep-20260924-000123", "amount": "1000", "currency": "TWD" }
FieldNotes
usernamePlayer username. A deposit to an unknown player creates it with currency; a withdrawal needs an existing player
txnIdYour transfer ID, 1–64 characters (letters, digits and _ . : -), used as the idempotency key
amountA decimal string greater than 0 with at most 4 decimals, for example "1000" or "0.5". Never a float
currencyMust equal the player's currency, otherwise CURRENCY_MISMATCH

Response:

json
{ "ok": true, "data": { "txnId": "dep-20260924-000123", "status": "done", "balance": "2500.5", "duplicate": false } }
  • Transfers are single-step: a success response means it is done (status is always done). There is no pending state and no confirm step.
  • balance is the balance right after this transfer.
  • A transfer that returned success can always be looked up: GET /wallet/transfer?txnId=…&username=….
  • Withdrawing more than the available balance returns 409 INSUFFICIENT_BALANCE and changes nothing.

Idempotency: txnId rules ​

CaseResult
New txnIdThe transfer runs, duplicate: false
Same txnId, same player, same amount and directionNot run again; returns the original result (balance as it was right after that transfer) with duplicate: true
Same txnId with a different amount or direction (deposit vs. withdrawal)409 TXN_CONFLICT, nothing changes
Same txnId already used for another player within the last two months409 TXN_CONFLICT, nothing changes
  • Keep txnId unique across your whole tenant, forever, for example dep-<your transaction number>.
  • Every new transfer gets a new txnId; a retry must reuse the same txnId with exactly the same content.

Timeouts and retries ​

After a network timeout, a dropped connection, a 5xx or a 429 you cannot know whether the transfer ran. Handle it like this:

  1. Before calling, record the transfer in your database (txnId, amount, direction, status "pending").
  2. Call deposit / withdraw (a 10-second timeout is reasonable).
  3. 2xx (including duplicate: true): mark it done and show the returned balance.
  4. 4xx (for example INSUFFICIENT_BALANCE, TXN_CONFLICT, INVALID_AMOUNT): a definite failure; mark it failed. For a failed withdrawal, give the amount back to the player's balance in your system.
  5. Timeout, network error, 5xx, 429: unknown. Resend with the same txnId and the same content (with a new nonce and timestamp each time, backing off 1, 2, 4, 8 seconds…) until you get a 2xx or a 4xx.
js
// Using call() from sign.js (see "Authentication & signing")
async function transferWithRetry(kind, payload, auth) {
  for (let attempt = 0; ; attempt++) {
    try {
      return await call('POST', `/api/tenant/v1/wallet/${kind}`, payload, auth); // 2xx: done
    } catch (err) {
      const unknown = !err.status || err.status >= 500 || err.status === 429;
      if (!unknown || attempt >= 8) throw err; // 4xx: definite failure; or hand over to a later job
      await new Promise((r) => setTimeout(r, Math.min(30_000, 1000 * 2 ** attempt)));
      // resend the same payload (same txnId)
    }
  }
}

You can also look it up first with GET /wallet/transfer?txnId=…&username=… (pass username):

  • 200: the transfer is done; the response has the direction, amount and balance afterwards. A transfer that returned success is always found.
  • 404 TXN_NOT_FOUND: the transfer was not executed (or is still being processed). Resend it with the same txnId until you get a definite answer; do not switch to a new txnId.

Never

Retry with a new txnId after a timeout. If the first attempt actually succeeded, the player is paid twice.

Looking up a transfer ​

text
GET /api/tenant/v1/wallet/transfer?txnId=dep-20260924-000123&username=alice
json
{
  "ok": true,
  "data": {
    "txnId": "dep-20260924-000123",
    "username": "alice",
    "dir": "in",
    "amount": "1000",
    "status": "done",
    "balance": "2500.5",
    "at": "2026-09-24T03:00:01.250Z"
  }
}
  • dir: in deposit, out withdrawal, adjust manual adjustment made in the Console.
  • username is optional but recommended: the player's wallet records the transfer the moment it completes, and the tenant ledger copy arrives shortly after. With username both are searched, so a successful transfer is always found.
  • The tenant ledger search covers the current month and the previous 3 months (UTC). For older transfers use the Console (Bets & transactions → Transfers) or an export.

Balance ​

text
GET /api/tenant/v1/wallet/balance?username=alice
json
{ "ok": true, "data": { "balance": "2500.5", "currency": "TWD" } }

This is the available balance, with stakes of open bets (and the hold on Niu Niu double bets) already deducted. To withdraw everything, read the balance and withdraw that amount; if the player bets between the two calls the withdrawal returns INSUFFICIENT_BALANCE, so just read again and retry.

Amounts and currency ​

  • All amounts are decimal strings with at most 4 decimals; responses drop trailing zeros ("100", "0.95", "1000.5").
  • Handle amounts as strings or a decimal type, never as floating-point numbers.
  • A player's currency is fixed when the player is created; transfers must use the same currency.

Daily reconciliation ​

  1. Transfers: every day, resend or look up each transfer in your records that is not "done" (same txnId) until every one has a definite result.

  2. Balances: for each active player, check that

    text
    opening balance + deposits − withdrawals + Σ bet win/loss (winLoss of the latest rev of each slip) ± manual adjustments = closing balance

    Bet win/loss comes from bet sync. If bets are still open at the time you reconcile, the difference equals their stakes (plus the hold for Niu Niu).

  3. Daily summary: compare GET /bets/summary?date= with your own totals.

Error codes ​

CodeHTTPCause
INVALID_PARAMETER400A field is missing, or txnId is malformed
INVALID_AMOUNT400The amount is not a positive string with at most 4 decimals
INVALID_USERNAME400The username breaks the rules
INVALID_CURRENCY400A deposit would create a player, but the currency code is invalid
CURRENCY_MISMATCH400Currency differs from the player's
PLAYER_NOT_FOUND404The player for a withdrawal or lookup does not exist
PLAYER_LIMIT403A deposit would create a player, but the plan's player limit is reached (50 on the free demo plan, 500 in the sandbox)
TXN_CONFLICT409txnId was used for a different transfer, or for another player within the last two months
INSUFFICIENT_BALANCE409Not enough available balance
TXN_NOT_FOUND404The transfer cannot be found (with username: it was not executed)

Common errors are listed in Error codes.

elite Tenant Integration API v1