Skip to content

Wallet API

The Wallet API is Surface 3: the three endpoints through which a game service triggers real money movements. All flow Service → Platform → Operator.

  • debit — take money when a bet is placed
  • credit — settle a bet (pay a win, or signal a loss with amount: 0)
  • rollback — reverse a debit on error

A game service never touches the operator wallet directly. The platform signs, validates, and retries; the service only makes an HTTP call to the platform and reads back a two-layer response.

sequenceDiagram
participant GS as Game Service
participant PL as Cloud1x2 Platform
participant OP as Partner Operator
GS->>PL: POST /internal/wallet/{debit|credit|rollback}
Note over PL: signs, adds hash
PL->>OP: signed wallet request
OP-->>PL: wallet reply (code, balance, status)
Note over PL: parses, evaluates httpOk
PL-->>GS: { ok, httpOk, data, log }

This is the most important idea on this page. Every wallet response has two success flags:

  • ok — the platform layer. true whenever the platform accepted the request, forwarded it, and got a reply (HTTP 200).
  • httpOk — the operator layer. true only when the operator wallet actually moved the money; false when the operator refused.

ok: true does not mean httpOk: true. A bet only succeeds when httpOk: true.

const r = await debit(payload);
if (!r.ok) throw new Error('platform error'); // platform-layer failure
if (!r.httpOk) {
// operator refused — insufficient funds, token expired, etc.
return rejectBet(r.data?.status ?? 'wallet refused');
}
// happy path — accept the bet, write the ledger
EndpointWhat it doesWhen
POST /internal/wallet/debitTake money from the playerWhen a bet request arrives, before anything else. On success the bet is accepted, the ledger opens, the engine receives it; on failure the bet is never accepted.
POST /internal/wallet/creditSignal settlement + (if any) pay a winWhen a real-mode bet settles. On a win amount = payout; on a loss amount = 0 (tells the operator the bet closed; no money moves).
POST /internal/wallet/rollbackReverse a debitAfter a debit, on an irrecoverable error (round cancelled, engine crash, …).

All three share one header:

Content-Type: application/json
x-internal-wallet-secret: <secret>

A wrong or missing secret returns 403 Forbidden for all three. Only rollback’s body omits the currency field; everything else is identical.

{
"partnerId": 1,
"betId": "wp9dvdgp4wfqmxq4-01krrh7xn3qq5e69gn58wkjt38",
"gameId": 9000,
"amount": 10.00,
"token": "d315fa12770e65eb4af38c589aed0d",
"trxId": "debit-wp9dvdgp4wfqmxq4-01krrh7xn3qq5e69gn58wkjt38",
"currency": "EUR",
"customer": "3773"
}
FieldTypeDescription
partnerIdintegerFrom the verify-launch response
betIdstringThe bet’s service_bet_uid ({service_uid}-{ulid}) — see Bet UID Format
gameIdintegerThe bet’s gameExternalId
amountnumberPositive, up to two decimals (10.00, 0.50)
tokenstringThe player’s launch token (from verify-launch)
trxIdstringThis wallet movement’s operator-side idempotency key. Recommended: "debit-" + betId
currencystring3-letter ISO 4217, uppercase
customerstringPlayer identity (from verify-launch)

betId and trxId differ: betId is the bet’s global identity; trxId is the identity of this one wallet movement (debit, credit, and rollback each get their own). Keep trxId ≤ 64 chars, ASCII only.

HTTP 200:

{
"ok": true,
"httpOk": true,
"data": {
"code": 0,
"status": "SUCCESS",
"balance": 990.00,
"bonusBalance": 0,
"currency": "EUR"
},
"log": {
"request": { "betId": "...", "gameId": 9000, "amount": 10, "token": "...", "trxId": "...", "customer": "3773" },
"response": { "code": 0, "status": "SUCCESS", "balance": 990, "bonusBalance": 0, "currency": "EUR" },
"requestTimestamp": "2026-05-19T10:00:00.123Z",
"responseTimestamp": "2026-05-19T10:00:00.245Z"
}
}
FieldMeaning
okPlatform-layer success. Always true on HTTP 200.
httpOkOperator-layer success. true = the wallet was debited.
data.codeOperator status code. 0 = success, negative = error.
data.statusOperator status string ("SUCCESS", "INSUFFICIENT_FUNDS", …).
data.balanceThe operator’s new balance after the debit.
data.bonusBalanceSeparate bonus wallet balance, if any; else 0.
data.currencyThe currency the operator confirmed; must match the request.
logAudit log — the full request/response bodies + timestamps.

HTTP 400 / 401 / 403 / 404 / 500, body:

{ "ok": false, "message": "<description>" }
HTTPExample messageMeaningResponse
400"Invalid partnerId"partnerId not a valid positive integerReject the bet; the body is malformed.
400"currency must be a 3-letter ISO code"Bad currency formatSame.
401"Forbidden"Wrong/missing x-internal-wallet-secretSecret misconfiguration.
403"currency not allowed for this partner"Not on the partner allowlistLaunch should already have been rejected — config drift.
404"Platform not found"No wallet config for this partnerMissing partner setup in the admin.
500(generic)Platform-side DB / logic errorRetry (wait ~5s, max 3 attempts); keep the bet pending.

HTTP 200, but httpOk: false:

{
"ok": true,
"httpOk": false,
"data": {
"code": -20102,
"status": "INSUFFICIENT_FUNDS",
"message": "balance below requested amount"
},
"log": { "request": { }, "response": { "code": -20102, "status": "INSUFFICIENT_FUNDS", "message": "..." }, "requestTimestamp": "...", "responseTimestamp": "..." }
}

The operator refused. Show the bet as rejected. Typical causes:

data.statusMeaningSuggested UX
INSUFFICIENT_FUNDSNot enough balance”Not enough balance”
TOKEN_INVALID / TOKEN_TIMEOUTSession invalid”Session expired” + disconnect
UNKNOWN_ERRORGeneric operator error”Wallet error” + retry hint
INVALID_GAMEGame not approved on the operatorLog; do not continue

The platform reduces the many operator response formats to the single httpOk boolean by treating a response as a failure when, for example: success === false; status/message matches patterns like insufficient, not enough, low balance, declined, rejected, token_timeout, session expired, unauthor, forbidden; or code is negative / in 400–599. Checking httpOk is enough.

Called when settling a bet. Same schema as debit; only trxId differs ("credit-" + betId).

{
"partnerId": 1,
"betId": "wp9dvdgp4wfqmxq4-01krrh7xn3qq5e69gn58wkjt38",
"gameId": 9000,
"amount": 19.50,
"token": "d315fa12770e65eb4af38c589aed0d",
"trxId": "credit-wp9dvdgp4wfqmxq4-01krrh7xn3qq5e69gn58wkjt38",
"currency": "EUR",
"customer": "3773"
}

amount is the win amount, not the staked amount (a 10 EUR bet at 1.95× credits 19.50).

Response: same structure as debit; data.balance is the post-credit balance.

Credit-side failures are rare but possible (token expired, operator downstream error, account suspended). On httpOk: false: do not settle as won; settle the ledger as lost/error, alert operations, and show the player “win recorded, payout delayed.” These corner cases need monitoring + alerting.

A debit happened but the bet could not proceed (round crash, engine crash, unexpected exception). Reverse the debit. The body omits currency — everything else matches debit:

{
"partnerId": 1,
"betId": "wp9dvdgp4wfqmxq4-01krrh7xn3qq5e69gn58wkjt38",
"gameId": 9000,
"amount": 10.00,
"token": "d315fa12770e65eb4af38c589aed0d",
"trxId": "rollback-wp9dvdgp4wfqmxq4-01krrh7xn3qq5e69gn58wkjt38",
"customer": "3773"
}

amount is the amount to return (the original debit). Currency is omitted because the operator knows it from the original trxId.

Response: same structure as debit/credit.

If rollback fails (debit taken, rollback refused — the worst case, the player’s money is “in the air”):

  1. Never loop retries forever. Exponential backoff, 3–5 attempts, then stop.
  2. Raise an alert (email, Slack, PagerDuty). Manual intervention is needed.
  3. Flag the ledger (for example status: "rollback_pending") so reporting can track it.

trxId is the operator’s idempotency key. Reusing the same trxId returns the same operator record — money never moves twice. Derive it deterministically from betId:

debit-<betId>
credit-<betId>
rollback-<betId>

Rules: stable and reproducible (derived from betId, not a random UUID generator), ≤ 64 chars, ASCII only. For games that pay more than once (cashout + final), vary the suffix: credit-<betId>-1, credit-<betId>-2.

Wallet calls pair with ledger calls. Order matters.

Opening a bet:

1. wallet debit
→ httpOk: false → reject the bet, do NOT open the ledger. Stop.
→ httpOk: true → continue.
2. open the ledger (status: open), passing walletB2bLog { kind: 'debit', entry: log }
3. add the bet to the engine, return 'accepted' to the player

Settling — won:

1. wallet credit (amount = win)
→ httpOk: false → settle the ledger status='lost', winAmount=0, alert
→ httpOk: true → settle the ledger status='won', winAmount=X
2. pass walletB2bLog { kind: 'credit', entry: log } on the settle call

Settling — lost:

1. wallet credit with amount=0 (signals the operator the bet closed)
2. settle the ledger status='lost', winAmount=0, walletB2bLog credit log

A loss still calls credit (amount: 0) for two reasons: it tells the operator the bet closed, and it keeps a symmetric open (debit) + close (credit) pair per bet — which keeps reporting and dispute resolution consistent.

Round crash (rollback):

1. wallet rollback
→ httpOk: true → settle the ledger status='rolled_back', set rollbackTrxId
→ httpOk: false → alert + manual follow-up (no infinite retry)
2. pass walletB2bLog { kind: 'rollback', entry: log } on the settle call

POST /internal/consolidated/tx and /settle accept an optional walletB2bLog field that attaches the wallet call’s audit log to the ledger:

{
"kind": "debit",
"entry": {
"request": { },
"response": { },
"requestTimestamp": "ISO-8601",
"responseTimestamp": "ISO-8601"
}
}

entry is exactly the log field from the wallet response — wrap it in kind and pass it through:

const debitR = await debit(payload);
await openLedger({
// ... ledger body ...
walletB2bLog: { kind: 'debit', entry: debitR.log },
});

This writes all operator I/O authoritatively to the platform ledger, so the service need not keep its own financial audit.

Assume the platform runs at http://localhost:7000.

Terminal window
# debit
curl -X POST http://localhost:7000/internal/wallet/debit \
-H "Content-Type: application/json" \
-H "x-internal-wallet-secret: dev" \
-d '{ "partnerId": 1, "betId": "wp9dvdgp4wfqmxq4-01krrh7xn3qq5e69gn58wkjt38", "gameId": 9000, "amount": 10.00, "token": "d315fa12770e65eb4af38c589aed0d", "trxId": "debit-wp9dvdgp4wfqmxq4-01krrh7xn3qq5e69gn58wkjt38", "currency": "EUR", "customer": "3773" }'

Credit uses /credit with amount = the win; rollback uses /rollback with the rollback- trxId and no currency.

ErrorCauseFix
400 Invalid partnerIdpartnerId not an integer / is 0Use the value from verify-launch directly
400 currency must be a 3-letter ISO codeLowercase/empty/4-char currencyUse the currency from verify-launch
403 currency not allowedNot on the partner allowlistLaunch should have been rejected — config drift
404 Platform not foundMissing wallet configSet the partner’s wallet config in the admin
httpOk: false, INSUFFICIENT_FUNDSLow balanceReject the bet, “Not enough balance”
httpOk: false, TOKEN_TIMEOUTLaunch token expiredClose the session, “Login again”
Timeout (no response)Operator downstream slowRetry 3–5× exponential backoff, then alert