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.
| Endpoint | Purpose |
|---|---|
POST /wallet/deposit | Deposit (your system → game wallet); creates the player if needed |
POST /wallet/withdraw | Withdraw (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
{ "username": "alice", "txnId": "dep-20260924-000123", "amount": "1000", "currency": "TWD" }| Field | Notes |
|---|---|
username | Player username. A deposit to an unknown player creates it with currency; a withdrawal needs an existing player |
txnId | Your transfer ID, 1–64 characters (letters, digits and _ . : -), used as the idempotency key |
amount | A decimal string greater than 0 with at most 4 decimals, for example "1000" or "0.5". Never a float |
currency | Must equal the player's currency, otherwise CURRENCY_MISMATCH |
Response:
{ "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 (
statusis alwaysdone). There is no pending state and no confirm step. balanceis 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_BALANCEand changes nothing.
Idempotency: txnId rules
| Case | Result |
|---|---|
New txnId | The transfer runs, duplicate: false |
Same txnId, same player, same amount and direction | Not 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 months | 409 TXN_CONFLICT, nothing changes |
- Keep
txnIdunique across your whole tenant, forever, for exampledep-<your transaction number>. - Every new transfer gets a new
txnId; a retry must reuse the sametxnIdwith 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:
- Before calling, record the transfer in your database (
txnId, amount, direction, status "pending"). - Call
deposit/withdraw(a 10-second timeout is reasonable). - 2xx (including
duplicate: true): mark it done and show the returnedbalance. - 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. - Timeout, network error, 5xx, 429: unknown. Resend with the same
txnIdand 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.
// 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 sametxnIduntil you get a definite answer; do not switch to a newtxnId.
Never
Retry with a new txnId after a timeout. If the first attempt actually succeeded, the player is paid twice.
Looking up a transfer
GET /api/tenant/v1/wallet/transfer?txnId=dep-20260924-000123&username=alice{
"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:indeposit,outwithdrawal,adjustmanual adjustment made in the Console.usernameis optional but recommended: the player's wallet records the transfer the moment it completes, and the tenant ledger copy arrives shortly after. Withusernameboth 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
GET /api/tenant/v1/wallet/balance?username=alice{ "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
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.Balances: for each active player, check that
textopening balance + deposits − withdrawals + Σ bet win/loss (winLoss of the latest rev of each slip) ± manual adjustments = closing balanceBet 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).
Daily summary: compare
GET /bets/summary?date=with your own totals.
Error codes
| Code | HTTP | Cause |
|---|---|---|
INVALID_PARAMETER | 400 | A field is missing, or txnId is malformed |
INVALID_AMOUNT | 400 | The amount is not a positive string with at most 4 decimals |
INVALID_USERNAME | 400 | The username breaks the rules |
INVALID_CURRENCY | 400 | A deposit would create a player, but the currency code is invalid |
CURRENCY_MISMATCH | 400 | Currency differs from the player's |
PLAYER_NOT_FOUND | 404 | The player for a withdrawal or lookup does not exist |
PLAYER_LIMIT | 403 | A deposit would create a player, but the plan's player limit is reached (50 on the free demo plan, 500 in the sandbox) |
TXN_CONFLICT | 409 | txnId was used for a different transfer, or for another player within the last two months |
INSUFFICIENT_BALANCE | 409 | Not enough available balance |
TXN_NOT_FOUND | 404 | The transfer cannot be found (with username: it was not executed) |
Common errors are listed in Error codes.