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 }The two-layer response
Section titled “The two-layer response”This is the most important idea on this page. Every wallet response has two success flags:
ok— the platform layer.truewhenever the platform accepted the request, forwarded it, and got a reply (HTTP 200).httpOk— the operator layer.trueonly when the operator wallet actually moved the money;falsewhen 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 failureif (!r.httpOk) { // operator refused — insufficient funds, token expired, etc. return rejectBet(r.data?.status ?? 'wallet refused');}// happy path — accept the bet, write the ledgerThe three endpoints
Section titled “The three endpoints”| Endpoint | What it does | When |
|---|---|---|
POST /internal/wallet/debit | Take money from the player | When 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/credit | Signal settlement + (if any) pay a win | When 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/rollback | Reverse a debit | After a debit, on an irrecoverable error (round cancelled, engine crash, …). |
All three share one header:
Content-Type: application/jsonx-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.
POST /internal/wallet/debit
Section titled “POST /internal/wallet/debit”Request body
Section titled “Request body”{ "partnerId": 1, "betId": "wp9dvdgp4wfqmxq4-01krrh7xn3qq5e69gn58wkjt38", "gameId": 9000, "amount": 10.00, "token": "d315fa12770e65eb4af38c589aed0d", "trxId": "debit-wp9dvdgp4wfqmxq4-01krrh7xn3qq5e69gn58wkjt38", "currency": "EUR", "customer": "3773"}| Field | Type | Description |
|---|---|---|
partnerId | integer | From the verify-launch response |
betId | string | The bet’s service_bet_uid ({service_uid}-{ulid}) — see Bet UID Format |
gameId | integer | The bet’s gameExternalId |
amount | number | Positive, up to two decimals (10.00, 0.50) |
token | string | The player’s launch token (from verify-launch) |
trxId | string | This wallet movement’s operator-side idempotency key. Recommended: "debit-" + betId |
currency | string | 3-letter ISO 4217, uppercase |
customer | string | Player 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.
Response — success path
Section titled “Response — success path”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" }}| Field | Meaning |
|---|---|
ok | Platform-layer success. Always true on HTTP 200. |
httpOk | Operator-layer success. true = the wallet was debited. |
data.code | Operator status code. 0 = success, negative = error. |
data.status | Operator status string ("SUCCESS", "INSUFFICIENT_FUNDS", …). |
data.balance | The operator’s new balance after the debit. |
data.bonusBalance | Separate bonus wallet balance, if any; else 0. |
data.currency | The currency the operator confirmed; must match the request. |
log | Audit log — the full request/response bodies + timestamps. |
Response — platform error path
Section titled “Response — platform error path”HTTP 400 / 401 / 403 / 404 / 500, body:
{ "ok": false, "message": "<description>" }| HTTP | Example message | Meaning | Response |
|---|---|---|---|
| 400 | "Invalid partnerId" | partnerId not a valid positive integer | Reject the bet; the body is malformed. |
| 400 | "currency must be a 3-letter ISO code" | Bad currency format | Same. |
| 401 | "Forbidden" | Wrong/missing x-internal-wallet-secret | Secret misconfiguration. |
| 403 | "currency not allowed for this partner" | Not on the partner allowlist | Launch should already have been rejected — config drift. |
| 404 | "Platform not found" | No wallet config for this partner | Missing partner setup in the admin. |
| 500 | (generic) | Platform-side DB / logic error | Retry (wait ~5s, max 3 attempts); keep the bet pending. |
Response — operator error path
Section titled “Response — operator error path”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.status | Meaning | Suggested UX |
|---|---|---|
INSUFFICIENT_FUNDS | Not enough balance | ”Not enough balance” |
TOKEN_INVALID / TOKEN_TIMEOUT | Session invalid | ”Session expired” + disconnect |
UNKNOWN_ERROR | Generic operator error | ”Wallet error” + retry hint |
INVALID_GAME | Game not approved on the operator | Log; 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.
POST /internal/wallet/credit
Section titled “POST /internal/wallet/credit”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.
POST /internal/wallet/rollback
Section titled “POST /internal/wallet/rollback”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”):
- Never loop retries forever. Exponential backoff, 3–5 attempts, then stop.
- Raise an alert (email, Slack, PagerDuty). Manual intervention is needed.
- Flag the ledger (for example
status: "rollback_pending") so reporting can track it.
trxId strategy (idempotency)
Section titled “trxId strategy (idempotency)”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.
Coordinating with the ledger
Section titled “Coordinating with the ledger”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 playerSettling — won:
1. wallet credit (amount = win) → httpOk: false → settle the ledger status='lost', winAmount=0, alert → httpOk: true → settle the ledger status='won', winAmount=X2. pass walletB2bLog { kind: 'credit', entry: log } on the settle callSettling — lost:
1. wallet credit with amount=0 (signals the operator the bet closed)2. settle the ledger status='lost', winAmount=0, walletB2bLog credit logA 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 callThe walletB2bLog audit field
Section titled “The walletB2bLog audit field”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.
curl examples
Section titled “curl examples”Assume the platform runs at http://localhost:7000.
# debitcurl -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.
Common mistakes
Section titled “Common mistakes”| Error | Cause | Fix |
|---|---|---|
400 Invalid partnerId | partnerId not an integer / is 0 | Use the value from verify-launch directly |
400 currency must be a 3-letter ISO code | Lowercase/empty/4-char currency | Use the currency from verify-launch |
403 currency not allowed | Not on the partner allowlist | Launch should have been rejected — config drift |
404 Platform not found | Missing wallet config | Set the partner’s wallet config in the admin |
httpOk: false, INSUFFICIENT_FUNDS | Low balance | Reject the bet, “Not enough balance” |
httpOk: false, TOKEN_TIMEOUT | Launch token expired | Close the session, “Login again” |
| Timeout (no response) | Operator downstream slow | Retry 3–5× exponential backoff, then alert |
Related
Section titled “Related”- Config + Ledger — opening and settling the ledger
- Gameplay Protocol — the WebSocket gameplay surface
- Bet UID Format — minting bet UIDs