--- title: "Connect module: wallet" description: The seamless wallet OneHazel calls on behalf of every game studio. Headers, signature, conformance scenarios and samples. --- For the operator's engineer building the wallet module. # wallet The wallet module is your seamless wallet, exposed once in the shape of the contract. Every game studio in the Marketplace reaches your balances through it: OneHazel translates each studio's protocol into these calls, so you never build a per-studio wallet. ## Who calls whom OneHazel calls you. You host the operations under the base URL you register for `wallet`. | Operation | What it does | Idempotency key | |---|---|---| | `POST /session/start` | Mint a single-use launch token for a player and game | yes | | `POST /session/exchange` | Exchange the launch token for a wallet-capable session token | yes | | `POST /session/balance` | Read the live balance for a session | no | | `POST /wallet/bet` | Debit a real-money stake | yes | | `POST /wallet/win` | Credit a win | yes | | `POST /wallet/rollback` | Reverse a prior bet or win by the amount you stored | yes | | `POST /freerounds/issue`, `/freerounds/cancel`, `/freerounds/settle` | Record, cancel and lump-settle a free-round grant | yes | | `POST /freerounds/get` | Read a grant's current state | no | The full request and response shapes are in the [contract](/connect/contract) and the OpenAPI document at `/openapi/connect-v1.yaml`. ## Headers and signature Every call carries three headers. Check them in this order, over the **raw** request body, before you parse it. | Header | Value | Reject with | |---|---|---| | `Authorization` | `Bearer `, the `cck_` value shown once at registration, or the `callbackKey` you supplied | `401 unauthorized` | | `X-OneHazel-Signature` | `t=,v1=`, where the hex is HMAC-SHA256 of `"." + raw body` keyed by your `whsec_` signing secret | `401 signature_invalid` when the MAC does not match or `t` is more than 300 s from your clock | | `X-OneHazel-Env` | `test` or `live` | an error when it is missing; route `test` to your test player or sandbox ledger only | Compare the MAC in constant time. During a secret rotation accept both the new and the previous secret for one hour. This is deliberately a different format from [webhook.receive](/connect/modules/webhook-receive), which keeps `sha256=` with no bearer and no timestamp. ## Money and idempotency - Amounts are integer minor units (`amount_minor`, `balance_minor`) at the currency's exponent: `100` is 1.00 EUR but 100 JPY. A code outside the contract's table is `400 invalid_request` naming the code; a listed currency that is not the player's is `422 currency_mismatch`. Never round. See [Currencies and exponents](/connect/contract#currencies-and-exponents). - Every money-moving call carries the same key in the `Idempotency-Key` header and the `idempotency_key` body field. A replay returns the **original** response with `duplicate: true`; the same key with a different body is `409 idempotency_key_conflict`. - Free-round bets never debit, so OneHazel never sends `wallet/bet` for them. `freerounds/settle` carries the lump credit itself and is never paired with a `wallet/win` for the same grant. ## Registration Register the base URL with a `testPlayerId`: a funded player in your `test` ledger. Wallet registration is refused without one, because conformance moves test money against that player and nobody else. ## Conformance A run sends every call signed, with `X-OneHazel-Env: test`, against your test player only. | Scenario | Severity | |---|---| | `auth.bad_bearer` | blocker | | `auth.bad_signature` | blocker | | `auth.stale_timestamp` | blocker | | `balance.ok` | blocker | | `bet.ok` | blocker | | `win.ok` | blocker | | `bet.insufficient_funds` | blocker | | `bet.duplicate_returns_original` | blocker | | `rollback.bet` | blocker | | `rollback.win` | blocker | | `rollback.amount_mismatch` | major | | `freerounds.bet_no_debit` | major | | `freerounds.settle_lump` | major | | `env.test_header_required` | blocker | | `shape.empty_body_is_failure` | blocker | What passing looks like for each one is on [Conformance](/connect/conformance#wallet). The module is `conformant` when the latest finished run passed. ## Samples | Stack | Sample | |---|---| | TypeScript + Express | [wallet](/connect/samples/typescript/express/wallet) | | TypeScript + Fastify | [wallet](/connect/samples/typescript/fastify/wallet) | | Python + FastAPI | [wallet](/connect/samples/python/fastapi/wallet) | | PHP (no framework) | [wallet](/connect/samples/php/plain/wallet) | | PHP + Laravel | [wallet](/connect/samples/php/laravel/wallet) | See [Samples](/connect/samples) for how to run one locally.