1. Integration Overview
Hash Play is a game content provider that supplies merchants with Hash Play games such as Crash and Dice. The parties use a Hosted Wallet model:
- Hash Play holds player gaming funds. Each player has an independent hosted wallet account on the platform, addressed by
(operator_id, user_id)and mapped one-to-one to a player on the merchant platform. - Before or after a player enters a game, the merchant uses the wallet APIs to deposit funds into the hosted account. All in-game fund movements—including bets, payouts, and round rollbacks—are settled locally and in real time by Hash Play within the hosted account, with no callback to the merchant.
- The merchant may query the balance, reconcile transactions, and inspect betting orders at any time, and may withdraw funds when they need to be returned to the merchant platform. Partial withdrawals are supported.
- This process is transparent to players, who see real-time changes to their hosted wallet balance in the game.
The entire integration involves interactions in one direction only: every API is called proactively by the merchant against Hash Play.
| Action | Direction | Description |
|---|---|---|
| Launch a game | Your platform → Hash Play | After a player selects a game, the merchant system requests a game entry URL from Hash Play and opens it in the player's browser |
| Wallet operations | Your platform → Hash Play | The merchant calls deposit, withdrawal, balance, transaction, order, and operation-status APIs as needed to manage the player's hosted wallet |
| In-game settlement | Internal to Hash Play | Bets, payouts, and rollbacks are completed within the platform-hosted account. The merchant does not provide callbacks or process each round transaction |
Overall Interaction Flow
Click to enlarge
Note: The blue area represents the launch phase, which runs once whenever a player enters a game. The green area represents the gameplay and fund-management phase. After entering a game, the player interacts directly with Hash Play for betting and settlement; the hosted wallet is updated in real time without passing through the merchant. The merchant platform acts as an intermediary only when the player needs to deposit or withdraw funds, by calling
/wallet/depositor/wallet/withdraw. The merchant may initiate balance inquiries and transaction reconciliation at any time.
2. Responsibilities
Hash Play is responsible for:
- Providing game content and all in-game logic, including bet evaluation, outcome generation, and payout calculation.
- Providing the game launch API (
/launch) and wallet APIs (/wallet/*). - Maintaining each player's hosted wallet account and all fund transactions, while ensuring accounting consistency.
- Providing the game-code list, integration environment, and technical support.
The merchant is responsible for:
- Maintaining player accounts on the merchant platform and mapping each player to the corresponding Hash Play hosted account through a stable
user_id. - Providing game entry points on the merchant platform, calling the launch API, and distributing game URLs to players.
- Calling wallet APIs as required to deposit and withdraw funds, query balances, reconcile transactions, and inspect betting orders.
- Protecting the API Secret, signing API requests, and ensuring that only merchant servers can operate the merchant's funds.
The merchant does not need to expose any wallet callback API or implement per-bet debit, credit, or rollback logic. Hash Play performs all of these operations within the hosted account.
3. Environment Information
Before development begins, request the following information from Hash Play and verify all environment details in one place:
| Configuration | Sandbox | Production |
|---|---|---|
Hash Play Gateway Base URL (used by /launch, /list, and /wallet/*) | Provided by Hash Play for integration testing, for example https://sandbox.example.com/prod-api/customer/app-api/api/v1/game | https://api.example.com/prod-api/customer/app-api/api/v1/game |
| API Version | v1 (/api/v1/game/launch, /api/v1/game/list, /api/v1/game/wallet/*) | Same as Sandbox |
Test operator_id | Assigned by Hash Play (example: M001) | Official merchant code |
Test api_key / api_secret | Assigned by Hash Play | Production credentials are issued separately and must never be mixed with Sandbox credentials |
| Test games | Dice (the Dice game code and name are identical), etc. See Chapter 13: Game List | Enabled according to the merchant's contracted game scope |
| Test currency | USDT (B2B currently supports USDT only) | USDT |
| Merchant egress IP (Hash Play allowlist, required) | Provided by the merchant and registered by Hash Play; all requests return 4004 if it is not registered | Provided by the merchant and registered by Hash Play |
| Signature timestamp window | ±60 seconds (X-Timestamp is a millisecond timestamp and must differ from server time by no more than 60000 ms) | Same as Sandbox |
| Nonce deduplication window | Reusing the same random string within approximately two minutes is treated as a replay and rejected | Same as Sandbox |
Hash Play will provide advance notice of environment changes such as gateway addresses or IP allowlists. Never use Sandbox credentials in Production.
Base URL composition: The Base URL consists of the gateway domain, nginx prefix
prod-api, gateway routecustomer, application prefixapp-api, andapi/v1/game. The remaining chapters use{base_url}to represent it, with endpoint paths appended as follows:
- Game launch:
{base_url}/launch- Game list:
{base_url}/list- Wallet APIs:
{base_url}/wallet/deposit,/wallet/withdraw,/wallet/balance,/wallet/transactions,/wallet/orders,/wallet/operation/statusAlways use the integration URL supplied by Hash Play. Do not construct paths independently.
4. Integration Credentials and Configuration
After the agreement is signed, Hash Play assigns identity credentials to the merchant. The merchant does not need to register any callback URL with Hash Play.
4.1 Credentials Assigned by Hash Play
| Credential | Purpose |
|---|---|
Merchant code (operator_id, example: M001) | Unique merchant identifier in Hash Play |
API Key (api_key) | Public identity identifier included in request headers |
API Secret (api_secret) | Shared secret used for signing; stored only on both parties' servers and never transmitted over the network |
Security requirement: The API Secret is equivalent to an access key for fund APIs. Store it only in secure server-side storage. Never place it in frontend code, client applications, logs, or source repositories. Contact Hash Play immediately to rotate the secret if exposure is suspected.
4.2 Configuration Provided by the Merchant
The merchant does not need to provide a wallet API address. Only one configuration item is required:
| Configuration | Description |
|---|---|
| Merchant server egress IP | Provided by the merchant for registration in the Hash Play allowlist; required. Only allowlisted IPs may call the merchant's Hash Play /launch, /list, and /wallet/* APIs |
The IP allowlist is mandatory, not optional. If the platform cannot find an enabled allowlist record for the merchant, IP validation fails immediately and returns
4004. The allowlist supports exact IPv4 addresses and CIDR ranges such as203.0.113.0/24. Notify Hash Play in advance if the merchant egress IP changes because of scaling, data-center migration, or NAT changes; otherwise, all APIs will become unavailable.The Sandbox environment also requires IP allowlist registration. Provide the egress IP of the integration host or environment to Hash Play.
5. Security: API Signatures
Every communication between the parties must use the same signature-verification scheme to ensure that:
- The request source is trusted and has not been forged by a third party.
- The content has not been modified, including amount parameters in transit.
- The request has not been replayed, preventing historical requests from being resubmitted to trigger deposits or withdrawals.
5.1 Communication Rules
Every merchant request to Hash Play includes four dedicated request headers:
| Header | Meaning | Description |
|---|---|---|
X-API-Key | Merchant identifier | Set to the merchant API Key |
X-Timestamp | Request timestamp | Always a millisecond timestamp (13-digit Unix epoch milliseconds). Requests with a server-time difference greater than ±60 seconds (60000 ms) are rejected |
X-Nonce | One-time random string | Unique for every request. A 32-character random hexadecimal string, such as a UUID without hyphens, is recommended. The platform caches used random strings for approximately two minutes; reusing a string is treated as a replay attack and rejected immediately |
X-Signature | Signature | Calculated with the API Secret according to Section 5.2 |
Only after signature verification succeeds does the platform validate merchant status (
4005) and the egress IP allowlist (4004). A signature error therefore appears before an IP issue. Troubleshoot in this order:4002 → 4005 → 4004.
5.2 Signature Calculation (Single Algorithm Used by Both Parties)
The signature algorithm is the industry-standard HMAC-SHA256, which has standard implementations in all mainstream programming languages. Calculation consists of two steps:
Step 1: Build the canonical string.
- Take all top-level fields from the JSON request body and remove fields with empty values.
- Sort by field name in lexicographic order, then concatenate as
field=value&field=value&.... - Append the timestamp and random string in that order, separated by
&.
For example, given this deposit request body:
{
"user_id": "u_10086",
"request_id": "dep_20260908_0001",
"amount": "1000.00"
}
After sorting by field name and appending the timestamp and random string, the source text to sign is:
amount=1000.00&request_id=dep_20260908_0001&user_id=u_10086&1712345678123&f3a9c2e8d1b4...
APIs without a request body, such as
GET /list: There is no field section, so the signature string becomes&{timestamp}&{nonce}and starts with&. This is a common integration pitfall. Do not prepend any character other than this leading&.
Step 2: Use the API Secret as the key and apply HMAC-SHA256 to the source text. Encode the result as lowercase hexadecimal; this is the value of X-Signature.
Clarification about the "raw message": The signature input is always the canonical string above, not the raw bytes of the HTTP message. JSON whitespace, field order, and case differences therefore do not affect verification. The recommended implementation is: parse JSON → extract fields → reconstruct the canonical string, as shown in Chapter 6. "Do not reserialize" means that you must not convert the parsed object directly back into a JSON string and apply HMAC to that string; JSON is not the signing input for this protocol.
5.3 Important Notes and Common Integration Failures
- The timestamp unit is always milliseconds:
X-Timestampuses a millisecond value (13-digit Unix epoch milliseconds). The validation window is ±60 seconds (60000 milliseconds). Keep the server clock synchronized to standard time; NTP is recommended. - Never transmit amounts as JSON numbers: Fields such as
amount,balance,bet_amount,win_amount, andmult_valueare JSON strings in requests and responses, for example"1000.00". Do not send them as numbers such as"amount": 10.00. Numeric serialization may change the value to10.0, causing a mismatch with the merchant signature string and a4002response. When building the canonical string, concatenate the request-body value exactly as provided ("1000.00"→amount=1000.00) without numeric formatting. Convert it withNumber()only for frontend display. - Remove empty values: Fields whose values are
nullor empty strings do not participate in canonical-string construction. The platform follows the same rule, so"amount": nullis equivalent to omitting the field. - Field names are case-sensitive: Canonical-string and request-body field names use lowercase snake case, such as
user_id,request_id, andgame_code. Do not use camel case.
5.4 Standard Signature-Verification Failure Response
If Hash Play cannot verify a merchant request signature, it returns:
{
"code": 4002,
"message": "签名验证失败"
}
message is localized and may vary by language. Always use code for programmatic decisions. Exception: On signature failure, the game list API /list always returns {"code": 4002, "message": "SIGNATURE_VERIFY_FAILED"}.
5.5 Response Structure and Two Error Categories
Business APIs return flat JSON. Both success and business failure return HTTP 200, and code distinguishes the result (code = 0 means success):
{ "code": 0, "message": "success", "...": "各接口自有字段" }
Be aware that the two error categories use different field names. Your parser must support both:
| Error source | Response shape | Description |
|---|---|---|
| Business validation failure (signature, merchant, IP, amount, balance, parameter semantics, etc.) | { "code": <4001~4017>, "message": "..." } | See Section 8.8: Standard Response Codes |
| Framework-level error (parameter validation/deserialization failure, system exception, etc.) | { "code": 500, "msg": "..." } | The field name is msg, not message |
Validation behavior:
/launchand/exchangeapply annotation-based validation to the request body. Omitting required fields such asoperator_id,user_id, orcurrencyproduces a framework-level error ({code:500, msg}), not a business error code./wallet/*does not use annotation-based validation; missing fields generally return business codes such as4011. Only a completely missing or invalid JSON request body returns{code:500, msg}.Implementation recommendation: Check the HTTP status first, then test
code === 0. Read the display message withdata.message ?? data.msg.
6. Code Examples
The examples below cover signature generation, Launch requests, and wallet operations and may be used directly as implementation references.
const crypto = require('crypto');
// Signature generation: body is the request body object
function generateSignature(apiSecret, timestampMs, nonce, body) {
const sorted = {};
Object.keys(body || {})
.filter((k) => body[k] !== null && body[k] !== undefined && body[k] !== '')
.sort()
.forEach((k) => (sorted[k] = String(body[k])));
const canonical = Object.entries(sorted)
.map(([k, v]) => `${k}=${v}`)
.join('&');
const signText = `${canonical}&${timestampMs}&${nonce}`;
return crypto
.createHmac('sha256', apiSecret)
.update(signText, 'utf8')
.digest('hex'); // Lowercase hexadecimal
}
// Call /launch (wallet /wallet/* APIs work the same way; only the path and request body change)
const axios = require('axios');
async function launch(apiSecret, apiKey, payload) {
const timestamp = String(Date.now()); // Milliseconds (13 digits)
const nonce = crypto.randomUUID().replace(/-/g, '');
const signature = generateSignature(apiSecret, timestamp, nonce, payload);
const resp = await axios.post(
'https://api.example.com/prod-api/customer/app-api/api/v1/game/launch',
payload,
{
headers: {
'Content-Type': 'application/json',
'X-API-Key': apiKey,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': signature,
},
}
);
return resp.data; // { code: 0, message: 'success', game_url: '...', balance: '0' }
}
// Call /wallet/deposit: fund the player's hosted wallet
// request_id is the unique order number for this deposit (generated by the merchant); the platform enforces idempotency by (operator_id, request_id)
async function deposit(apiSecret, apiKey, userId, requestId, amount) {
// Pass amounts as strings to avoid signature failures caused by floating-point serialization differences
const payload = { user_id: userId, request_id: requestId, amount: String(amount) };
const timestamp = String(Date.now());
const nonce = crypto.randomUUID().replace(/-/g, '');
const signature = generateSignature(apiSecret, timestamp, nonce, payload);
const resp = await axios.post(
'https://api.example.com/prod-api/customer/app-api/api/v1/game/wallet/deposit',
payload,
{
headers: {
'Content-Type': 'application/json',
'X-API-Key': apiKey,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': signature,
},
}
);
return resp.data; // { code: 0, message: 'success', currency: 'USDT', amount: '1000.00', balance: '1000.00', txn_id: '...' }
}
// Call /wallet/operation/status: verify whether a deposit or withdrawal succeeded by merchant order number (use after a timeout or unknown result)
async function operationStatus(apiSecret, apiKey, requestId, operationType) {
const payload = { request_id: requestId, operation_type: operationType }; // 'DEPOSIT' | 'WITHDRAW'
const timestamp = String(Date.now());
const nonce = crypto.randomUUID().replace(/-/g, '');
const signature = generateSignature(apiSecret, timestamp, nonce, payload);
const resp = await axios.post(
'https://api.example.com/prod-api/customer/app-api/api/v1/game/wallet/operation/status',
payload,
{
headers: {
'Content-Type': 'application/json',
'X-API-Key': apiKey,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': signature,
},
}
);
return resp.data; // { code: 0, status: 1, amount: '1000.00', txn_id: '...' }
}
// Call GET /list (game list): with no request body, the signature string becomes '×tamp&nonce'
async function gameList(apiSecret, apiKey, language = 'zh-CN') {
const timestamp = String(Date.now());
const nonce = crypto.randomUUID().replace(/-/g, '');
const signature = generateSignature(apiSecret, timestamp, nonce, null); // Pass null as body
const resp = await axios.get(
`https://api.example.com/prod-api/customer/app-api/api/v1/game/list?language=${encodeURIComponent(language)}`,
{
headers: {
'X-API-Key': apiKey,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': signature,
},
}
);
return resp.data; // { code: 0, games: [...] }
}7. Game Launch
7.1 Business Flow
- The player selects a game on the merchant platform.
- The merchant backend requests a game entry URL from Hash Play. Never call this API directly from the frontend, because doing so would expose the Secret.
- After verifying the signature and parameters, Hash Play finds or creates the platform user and hosted wallet for
(operator_id, user_id). It generates and returns a single-use entry URL containing a one-time ticket, together with the player's current hosted balance on the platform. - The merchant sends the URL to the player's browser, which may open it in a new window or embedded view.
An entry URL is valid for one use only and expires after a limited period. Its ticket is valid for five minutes and becomes invalid immediately after redemption. Never cache or reuse it, and never share one URL among multiple players. Request a new URL whenever a player enters a game; the API has minimal overhead.
Idempotency:
/launchfinds or creates a platform user and hosted wallet by(operator_id, user_id). Repeated calls do not create duplicate users or change the balance. It is safe to call repeatedly, but each returnedgame_urlmust be opened separately.Balance source: The returned
balanceis the player's current platform-hosted balance as a string. It is usually"0"on first launch. Call/wallet/depositafterward to fund the player, as described in Chapter 8.
7.1.1 Behavior When game_code Is Omitted
game_code is optional, with two behaviors:
Is game_code provided? | Platform behavior |
|---|---|
Provided (for example, Dice) | Resolve the game identifier through the game_type dictionary → verify merchant-by-game permission (4006 if unauthorized) and limit configuration (4008 if missing) → return an entry URL for that game |
| Omitted / empty string | Treat the game as undetermined → skip game-level permission and limit validation while retaining signature, merchant-status, and IP-allowlist checks → return the generic game lobby URL, where the player chooses a game |
If the merchant wants to hide unauthorized games in the lobby, it should still provide
game_codeso that permission validation runs. Ifgame_codeis omitted, the platform separately determines whether the player may start a selected game after entering the lobby.
game_codeis matched case-insensitively against the label (dictLabel) in thegame_typedictionary;crash,CRASH, andCrashare equivalent. If a label contains spaces, its first word may be used as shorthand. The safest approach is always to use the exactgameCodevalue returned by the Game List API, without modifying it.
7.2 API Definition
Merchant request:
POST {base_url}/api/v1/game/launch
Request parameters (JSON with lowercase snake-case field names):
| Parameter | Required | Visible to | Description |
|---|---|---|---|
operator_id | Yes | System | Merchant code, for example M001 |
user_id | Yes | System | Unique player account identifier on the merchant platform. Always send the same ID for the same player. Hash Play uses (operator_id, user_id) to locate or create the platform user and hosted wallet |
game_code | No | System | Game code from the list supplied by Hash Play, for example Crash. See Chapter 13 for the full list and real-time API. Omit it to open the game lobby |
currency | Yes | System | Player currency code; must be USDT. B2B currently uses USDT only. Although the platform ignores the value and other values do not change the settlement currency, this field remains required |
username | No | Player | Player nickname. Displayed in the game when provided; otherwise, the system default is shown |
avatar | No | Player | URL of the player's avatar image. Displayed in the game when provided |
language | No | Player | Game UI language; defaults to English when omitted. Use an identifier from the SimpleLocalize locale list in language-country format, such as zh-CN, pt-BR, or th-TH. Common values are listed below |
return_url | No | Player | Merchant-platform URL to return to after the player exits the game, for example https://yourdomain.com/lobby |
Common language values (see the SimpleLocalize locale list for the complete list):
| Language | Code | Language | Code | Language | Code |
|---|---|---|---|---|---|
| Simplified Chinese | zh-CN | Traditional Chinese (Taiwan) | zh-TW | Traditional Chinese (Hong Kong) | zh-HK |
| English (United States) | en-US | English (United Kingdom) | en-GB | Spanish (Spain) | es-ES |
| Japanese | ja-JP | Korean | ko-KR | Vietnamese | vi-VN |
| Thai | th-TH | Indonesian | id-ID | Russian | ru-RU |
| Turkish | tr-TR | Arabic | ar-SA | Hindi | hi-IN |
Before sending a locale, confirm with Hash Play that its game language pack is available. Unsupported locales fall back to the English UI.
Request example:
{
"operator_id": "M001",
"user_id": "u_10086",
"game_code": "Crash",
"currency": "USDT",
"username": "玩家A",
"language": "zh-CN",
"return_url": "https://yourdomain.com/lobby"
}
Hash Play response:
{
"code": 0,
"message": "success",
"game_url": "https://game.hashplay.io/hashWeb/#/zh-CN?ticket=8f3e...c21a",
"balance": "1000.00"
}
| Field | Description |
|---|---|
game_url | Single-use entry URL to open in the player's browser |
balance | Player's current platform-hosted balance as a string, such as "1000.00"; usually "0" on first launch |
Failure response ({code, message, game_url: null}):
| Code | Meaning | Merchant action |
|---|---|---|
0 | Success | Send game_url to the frontend for opening. balance is the player's current hosted balance, usually "0" on first launch, and can be used to decide whether to prompt for a deposit |
4002 | Signature verification failed | Check the signature implementation (see Section 5.3). Do not retry until corrected |
4004 | IP is not allowlisted | The merchant egress IP is unregistered or has changed. Contact Hash Play to verify it; do not retry |
4005 | Merchant is invalid or disabled | Verify operator_id and merchant status, then contact Hash Play |
4006 | Game does not exist or merchant lacks permission | game_code is not in the dictionary or is not enabled for the merchant. Use a game with status=1 from the Game List API, or omit game_code to open the lobby |
4008 | Game limits are not configured | Limits are missing for the merchant-game combination. Contact Hash Play to enable them |
4009 | Game URL is missing or disabled | The game entry URL is not configured. Contact Hash Play |
4010 | Player is disabled | The platform user associated with user_id is disabled or deleted. Notify the player according to your business process |
500 | System error | Use message to display a suitable player-facing message such as "The game is temporarily unavailable," then retry later |
Except for
500, retrying the same failed request will probably fail again. Diagnose the cause using the table before resending, and avoid blind polling. A general player-facing message such as "The game is temporarily unavailable. Please try again later." is sufficient.
8. Wallet APIs
After the player enters a game, Hash Play settles all in-game fund movements—including bet debits, payout credits, and abnormal-round refunds—locally and in real time within the player's hosted wallet. The merchant does not need to observe these events or provide callbacks. The merchant manages hosted funds using the six wallet APIs described below.
8.1 Semantics of the Six Wallet APIs
| API | Method | When to call | Description |
|---|---|---|---|
Deposit /wallet/deposit | POST | Before or after game entry, when the merchant decides to fund the player | Adds amount to the hosted balance and writes a DEPOSIT transaction |
Withdraw /wallet/withdraw | POST | When the player exits or the merchant recovers funds | Withdraws a specified amount or the entire balance and writes a WITHDRAW transaction |
Balance /wallet/balance | POST | At any time, such as balance display or risk-control checks | Returns the player's current hosted balance |
Transactions /wallet/transactions | POST | For reconciliation or customer-support investigations | Queries all player fund movements in a time range, including in-game bets, payouts, and rollbacks |
Orders /wallet/orders | POST | For reconciliation, customer-support investigations, or betting analysis | Queries player betting orders in a time range, including game code, bet/payout amount, settlement status, multiplier, and round details |
Operation status /wallet/operation/status | POST | After a deposit/withdrawal timeout or unknown result | Determines success by merchant request_id and operation type |
All six APIs are under {base_url}/api/v1/game/wallet. Except for operation status, they address a player by (operator_id, user_id). The credential associated with X-API-Key determines operator_id, so it is not included in the request body. Operation status queries by (operator_id, request_id, operation_type). For transaction and order queries, user_id is optional; when omitted, the query covers all users of the merchant, and each response record includes user_id. Authentication is identical to /launch: four signed headers plus the IP allowlist.
8.2 Deposit—Fund a Player
Merchant request:
POST {base_url}/wallet/deposit
Request parameters:
| Parameter | Required | Description |
|---|---|---|
user_id | Yes | Player account identifier on the merchant platform; must match the value sent to /launch |
request_id | Yes | Merchant order number, uniquely identifying this deposit; a format such as dep_date_sequence is recommended. The platform enforces idempotency by (operator_id, request_id). A successfully used request_id must never be reused |
amount | Yes | Deposit amount as a positive string with two decimal places, such as "1000.00" |
request_idis the idempotency key for deposit and withdrawal APIs and the query key used by/wallet/operation/status. Generate and persist it on the merchant side, linked to the merchant's own deposit order. A maximum length of 64 characters is recommended.
request_idis not a random string: it must uniquely identify a business transaction. Reuse the samerequest_idwhen resending after a network timeout. Use a new value for every new deposit; otherwise, the request is rejected with4014.
Request example:
{
"user_id": "u_10086",
"request_id": "dep_20260908_0001",
"amount": "1000.00"
}
Hash Play response:
{
"code": 0,
"message": "success",
"currency": "USDT",
"amount": "1000.00",
"balance": "1000.00",
"txn_id": "txn_20260904_0001"
}
amount is the deposited amount, balance is the latest hosted balance after the deposit, and txn_id is the transaction ID generated by Hash Play for reconciliation. All amount fields are strings. See Section 5.3.
Possible deposit error codes:
| Code | Meaning | Merchant action |
|---|---|---|
4001 | Invalid amount | amount is missing or ≤ 0. Correct the parameter and retry |
4011 | Empty user_id | Add the parameter and retry |
4012 | Invalid user_id | No user exists for (operator_id, user_id). Call /launch first to create the user |
4013 | Empty request_id | Add the parameter and retry |
4014 | This request_id has already succeeded | Do not execute it again. Verify with /wallet/operation/status, or use a new request_id for a new transaction |
4015 | Wallet operation failed | The failure was recorded with status=0; retry with the original request_id |
8.3 Withdraw—Recover Funds
Merchant request:
POST {base_url}/wallet/withdraw
Request parameters:
| Parameter | Required | Description |
|---|---|---|
user_id | Yes | Player account identifier on the merchant platform; must match the value sent to /launch |
request_id | Yes | Merchant order number uniquely identifying this withdrawal; a format such as wd_date_sequence is recommended. The platform enforces idempotency by (operator_id, request_id) |
amount | No | Withdrawal amount as a positive string with two decimal places, such as "500.00"; omit it to withdraw the entire balance and set the balance to zero |
For a full withdrawal, omit
amountor pass an empty string. Do not query the balance first and then send that value; game settlement could occur between the two calls and cause an amount mismatch that returns4001.
Request example (partial withdrawal):
{
"user_id": "u_10086",
"request_id": "wd_20260908_0001",
"amount": "500.00"
}
Request example (full withdrawal):
{
"user_id": "u_10086",
"request_id": "wd_20260908_0002"
}
Hash Play response:
- Successful withdrawal:
{ "code": 0, "message": "success", "currency": "USDT", "amount": "500.00", "balance": "515.00", "txn_id": "txn_20260904_0002" }(amountis the withdrawn amount andbalanceis the latest balance after withdrawal; for a full withdrawal,balanceis"0.00") - Zero balance:
{ "code": 4003, "message": "WALLET_BALANCE_ZERO", "currency": "USDT", "balance": "0.00" }(there are no funds to withdraw; handle this according to the business scenario)
Withdraw supports partial withdrawals. Provide
amountto withdraw a specified amount; omit it to withdraw the entire balance and set it to zero. The amount may not exceed the current balance, or the platform returns4001for an invalid amount.
Possible withdraw error codes:
| Code | Meaning | Merchant action |
|---|---|---|
4001 | Invalid amount | amount is ≤ 0 or exceeds the current hosted balance, which may have changed because of concurrent settlement. Consider omitting amount to withdraw the full balance |
4003 | Hosted balance is zero | No funds are available; handle according to the business scenario, normally as already fully withdrawn |
4011 / 4012 | Empty / invalid user_id | Add the parameter or call /launch first to create the user |
4013 | Empty request_id | Add the parameter and retry |
4014 | This request_id has already succeeded | Verify with /wallet/operation/status, or use a new request_id for a new transaction |
4015 | Wallet operation failed | The failure was recorded with status=0; retry with the original request_id |
8.4 Balance
Merchant request:
POST {base_url}/wallet/balance
Request parameters:
{ "user_id": "u_10086" }
user_id is required, as in Section 8.3. A balance query does not move funds and requires no request_id.
Hash Play response:
{
"code": 0,
"message": "success",
"currency": "USDT",
"balance": "1015.00"
}
balanceis a string, such as"1015.00". Convert it withNumber()orBigDecimalbefore displaying it in the merchant frontend. Do not usetypeof === 'number'to conclude that the platform omitted the field.
Possible error codes: 4011 / 4012 (empty / invalid user_id, usually indicating that the player has not yet been created on the platform through /launch).
8.5 Transactions—Reconciliation
Merchant request:
POST {base_url}/wallet/transactions
Request parameters:
| Parameter | Required | Description |
|---|---|---|
user_id | No | Player account identifier on the merchant platform; omit it to query transactions for all merchant users |
start_time | No | Start time as an inclusive millisecond timestamp |
end_time | No | End time as an inclusive millisecond timestamp; must not be earlier than start_time |
page_num | No | Page number starting from 1; default: 1 |
page_size | No | Records per page; default: 10, maximum: 500 |
Hash Play response:
{
"code": 0,
"message": "success",
"currency": "USDT",
"page_num": 1,
"page_size": 10,
"total": 3,
"records": [
{
"user_id": "u_10086",
"txn_id": "txn_20260904_0001",
"txn_type": "DEPOSIT",
"amount": "1000.00",
"balance_before": "0.00",
"balance_after": "1000.00",
"round_id": null,
"ref_txn_id": null,
"remark": "deposit:M001",
"create_time": 1756948800000
},
{
"user_id": "u_10086",
"txn_id": "txn_20260904_0003",
"txn_type": "BET",
"amount": "10.00",
"balance_before": "1000.00",
"balance_after": "990.00",
"round_id": "r_20260904_0001",
"ref_txn_id": null,
"remark": null,
"create_time": 1756948900000
},
{
"user_id": "u_10086",
"txn_id": "txn_20260904_0004",
"txn_type": "WIN",
"amount": "25.00",
"balance_before": "990.00",
"balance_after": "1015.00",
"round_id": "r_20260904_0001",
"ref_txn_id": "txn_20260904_0003",
"remark": null,
"create_time": 1756948950000
}
]
}
| Field | Description |
|---|---|
page_num / page_size / total | Page number, records per page, and total record count |
user_id | Player account identifier on the merchant platform; it is the requested user_id, or identifies ownership in a cross-user query |
txn_id | Globally unique transaction ID |
txn_type | Transaction type: DEPOSIT (merchant deposit), WITHDRAW (merchant withdrawal), BET, WIN, ROLLBACK (abnormal-round refund), or another in-game fund movement |
amount | Positive changed amount as a string |
balance_before / balance_after | Balances before and after the movement, as strings |
round_id | Associated game round ID; null for deposit and withdrawal transactions |
ref_txn_id | Original related transaction ID, such as the bet corresponding to a payout or refund |
remark | Additional information |
create_time | Transaction time as a millisecond timestamp |
8.6 Orders—Betting Order Inspection
Merchant request:
POST {base_url}/wallet/orders
Request parameters (identical to the transaction query in Section 8.5):
| Parameter | Required | Description |
|---|---|---|
user_id | No | Player account identifier on the merchant platform; omit it to query orders for all merchant users |
start_time | No | Start time as an inclusive millisecond timestamp |
end_time | No | End time as an inclusive millisecond timestamp; must not be earlier than start_time |
page_num | No | Page number starting from 1; default: 1 |
page_size | No | Records per page; default: 10, maximum: 500 |
Hash Play response:
{
"code": 0,
"message": "success",
"currency": "USDT",
"page_num": 1,
"page_size": 10,
"total": 1,
"records": [
{
"user_id": "u_10086",
"round_id": "r_20260904_0001",
"game_code": "Crash",
"currency": "USDT",
"bet_amount": "10.00",
"win_amount": "25.00",
"status": "SETTLED",
"settle_result": 1,
"mult_value": "2.5",
"settle_time": 1756948950000,
"create_time": 1756948900000,
"game_detail": "{...}"
}
]
}
| Field | Description |
|---|---|
page_num / page_size / total | Page number, records per page, and total record count |
user_id | Player account identifier on the merchant platform; it is the requested user_id, or identifies ownership in a cross-user query |
round_id | Game round ID; corresponds to round_id in /wallet/transactions and supports cross-checking |
game_code | Game code, consistent with /launch and the Game List API |
currency | Currency |
bet_amount | Total bet amount for the round as a string |
win_amount | Payout amount for the round as a string |
status | Round status: OPEN, SETTLED, or CANCELLED |
settle_result | Settlement result: 1 for win / 0 for loss; present only for SETTLED rounds |
mult_value | Settlement multiplier as a string, such as "2.5"; depends on game rules and may be empty |
settle_time | Settlement time as a millisecond timestamp |
create_time | Round creation time as a millisecond timestamp |
game_detail | Game-specific round details as JSON, used for customer-support investigations; format varies by game |
Transactions in Section 8.5 record every fund movement, including deposits and withdrawals. Orders in this section record the bet and settlement result of each game round. They are linked by
round_idand together provide a complete reconstruction of player activity and fund movements.
8.7 Operation Status—Verify Whether a Merchant Order Succeeded
Merchant request:
POST {base_url}/wallet/operation/status
Request parameters:
| Parameter | Required | Description |
|---|---|---|
request_id | Yes | Merchant order number, identical to the request_id supplied for deposit/withdraw |
operation_type | Yes | DEPOSIT or WITHDRAW; case-insensitive. Other values return 4017 |
Request example:
{
"request_id": "dep_20260908_0001",
"operation_type": "DEPOSIT"
}
This API does not require
user_id. Arequest_idunique within the merchant scope is sufficient.
Hash Play response:
{
"code": 0,
"message": "success",
"currency": "USDT",
"request_id": "dep_20260908_0001",
"operation_type": "DEPOSIT",
"status": 1,
"amount": "1000.00",
"txn_id": "txn_20260904_0001",
"error_msg": null,
"create_time": 1756948800000
}
| Field | Description |
|---|---|
status | 1 for success / 0 for failure. If the same request_id has ever succeeded, the successful record takes precedence |
amount | Operation amount as a string; for a full withdrawal, this is the actual withdrawn amount |
txn_id | Associated wallet transaction ID on success; null on failure |
error_msg | Failure reason; null on success |
create_time | Record creation time as a millisecond timestamp |
Possible error codes: 4016 (no record found for request_id, either because it was never submitted or was misspelled) / 4017 (operation_type is neither DEPOSIT nor WITHDRAW).
After a timeout or unknown result, call this API first.
status=1means the credit/debit succeeded; do not resend the samerequest_id. Ifstatus=0or4016is returned, retry with the originalrequest_id.
8.8 Standard Response Codes
| Code | Meaning | Merchant action |
|---|---|---|
0 | Success | Includes the latest balance, and for fund movements, amount and txn_id |
4001 | Invalid amount | Deposit: missing amount or amount ≤ 0. Withdraw: amount exceeds the current balance. Correct and retry |
4002 | Signature verification failed | Check the signature implementation and Section 5.3 |
4003 | Hosted balance is zero | Returned only by withdraw. No funds are available; handle as a business result |
4004 | IP is not allowlisted | Merchant egress IP is unregistered. Contact Hash Play |
4005 | Merchant is invalid or disabled | Verify the API Key and merchant status, then contact Hash Play |
4006 | Invalid time range | Returned only by transactions/orders when start_time > end_time or a timestamp is not a millisecond numeric value |
4011 | Empty user_id | Add the parameter and retry |
4012 | Invalid user_id | No user exists for (operator_id, user_id). Call /launch first |
4013 | Empty request_id | Add the parameter and retry |
4014 | request_id already succeeded | Do not execute again. Verify with the status API or use a new order number |
4015 | Wallet operation failed | The failure was recorded; retry with the original request_id |
4016 | Wallet operation record not found | Only for /wallet/operation/status: no matching order number/type record |
4017 | Invalid operation_type | Only for /wallet/operation/status: only DEPOSIT / WITHDRAW are supported |
Same code, different meaning:
4006,4011,4012, and4013have different meanings under/launchand/wallet/*(ticket/game-permission errors in the former and parameter errors in the latter), and some messages are localized. Evaluate the API andcodetogether; never usecodealone:
code Meaning under /launchMeaning under /wallet/*4006Merchant lacks game permission (see Section 7.2) Invalid transactions/orders time range 4011Empty ticket(only/exchange)Empty user_id4012Invalid or expired ticketInvalid user_id(user not created)4013User for ticketdoes not existEmpty request_id
9. Complete Interaction Example
The following example shows a player completing one Crash round after the merchant makes an initial deposit of 1000.00 USDT:
Click to enlarge
Reconciliation formula (must hold at all times for merchant financial verification):
所有存入(Deposit) − 所有提走(Withdraw) − 所有投注(Bet) + 所有派奖(Win) + 所有退款(Rollback) = 当前托管余额(Balance)
In the diagram, the merchant deposits 1000 (balance: 0 → 1000.00), the player places an in-game bet debit of 10 (→ 990.00), receives a payout of 25 (→ 1015.00), and after the player exits, the merchant withdraws the full 1015.00 (balance set to zero). If the round is cancelled because of an error, the platform automatically adds a Rollback transaction that returns the original bet to the hosted balance without merchant intervention.
10. Merchant System Technical Requirements
The following are mandatory requirements for the hosted-wallet model and directly affect player experience and the security of both parties' funds:
- Hash Play transactions are authoritative: All in-game fund movements occur on the platform. Merchant finance teams must reconcile with
/wallet/transactionsplus/wallet/balance, using the formula in Chapter 9. - Keep
user_idstable and unique: Hosted wallets are addressed by(operator_id, user_id). Always use the same ID for the same player. Using another player's ID will operate that player's wallet. - Deposit before betting: Bets use the hosted balance. If it is insufficient, the platform rejects the bet and notifies the player. Deposit funds before game entry or when the balance is depleted.
- Partial withdrawals are supported: Provide
amountto withdraw a specified amount; omit it to withdraw the full balance and set it to zero.4003means the balance is already zero and is a normal business result, not a system failure. - Response performance: Deposit and balance calls are on the critical path for game entry. Keep merchant-side request paths reliable. Hash Play targets responses within 500 milliseconds.
- Sign before calling: Every request must contain a valid signature according to Chapter 5; failures return
4002. - The IP allowlist is mandatory: Register merchant egress IPs with Hash Play. Unregistered IPs cause all APIs to return
4004. IPv4 and CIDR are supported; notify Hash Play before egress changes. - Maintain an accurate clock: Synchronize servers with NTP. See Section 5.3 for timestamp and window rules.
- Use the correct amount type: Never transmit amounts as JSON numbers. See Section 5.3.
- Persist
request_idand retry safely: Persist the deposit/withdrawrequest_idas an idempotency key, following Section 8.2. After a timeout or unknown result, query/wallet/operation/statusfirst. Never resend with a new ID before verification.
11. Checklist
- Preparation: Obtain the merchant code, API Key, API Secret, and Sandbox/Production gateway addresses from Hash Play. See Chapter 3.
- Preparation: Provide the merchant server egress IP to Hash Play. This is mandatory and supports IPv4/CIDR; all APIs return
4004if it is not registered. - Preparation: Call
/api/v1/game/listto retrieve the currently enabled games. Do not hard-code a local game-code table, because platform availability changes. - Development: Implement the HMAC-SHA256 signing utility and pass Hash Play's signing test cases, especially lexicographic field ordering, empty-value removal, unmodified amount strings, and millisecond timestamps.
- Development: Implement server-side game launch through
/launchand distributegame_urlto the frontend. - Development: Implement
/wallet/deposit,/wallet/withdraw,/wallet/balance,/wallet/transactions,/wallet/orders, and/wallet/operation/status. - Development: Define and persist
request_id; validate required fields before deposit/withdraw; use/wallet/operation/statusafter timeouts. - Development: Process amount fields as strings end to end according to Section 5.3, converting with
Number()only for display. - Self-test: Normal flow—deposit → balance query → partial / full withdrawal.
- Self-test: Error flow—signature failure (
4002), unregistered IP (4004), zero-balance withdrawal (4003), invaliduser_id(4012), invalid amount (4001), missingrequest_id(4013), duplicaterequest_id(4014). - Self-test: Send a timestamp outside the ±60-second window, verify a
4002response, and confirm that NTP synchronization resolves it. - Integration testing: Complete the full flow with Hash Play: launch → deposit → bet → payout → transaction reconciliation → withdrawal.
- Go live: Switch to Production credentials, confirm the IP allowlist, and reconcile on the first day.
12. FAQ
Check the following in order:
- Timestamp: It must be a 13-digit millisecond value within 60 seconds of Hash Play time; NTP is recommended.
- Signature string: Verify lexicographic field order, empty-value removal, and the trailing
&{timestamp}&{nonce}. - Amount type: Never use JSON numbers; send strings. See Section 5.3.
- Signature case: Hexadecimal output must be lowercase.
- Nonce: Do not reuse it within approximately two minutes.
- GET without a request body, such as
/list: The signature string becomes&{timestamp}&{nonce}.
Test against the examples in Chapter 6.
13. Game List
13.1 Game List API (Recommended Real-Time Source)
The game list is continuously updated. Retrieve it through this API instead of hard-coding it in your system. Newly launched Hash Play games become visible immediately without waiting for a documentation update.
Merchant request:
GET {base_url}/api/v1/game/list?language=zh-CN
| Parameter | Required | Description |
|---|---|---|
language | No | Locale for game names, in the same format as /launch language, such as zh-CN. Defaults to English when omitted |
Signature: Identical to /launch: X-API-Key / X-Timestamp in milliseconds within ±60 seconds / X-Nonce / X-Signature. A GET request has no request body, so the signature string becomes ×tamp&nonce. See Section 5.2.
Hash Play response:
{
"code": 0,
"message": "success",
"games": [
{
"gameCode": "Crash",
"gameName": "爆点",
"status": 1,
"supportedCurrency": "USDT",
"thumbnail": "https://resource.hashplay.io/xxx.png",
"isMultiplayer": 0,
"sort": 1
}
]
}
| Field | Description |
|---|---|
gameCode | Game code supplied as /launch game_code. Matching is case-insensitive; if the label contains spaces, its first word is accepted as shorthand. The value comes from the platform game_type dictionary label and may be renamed by operations, so never cache it in local code |
gameName | Localized game name for language; falls back to English if no translation exists |
status | 1 = enabled for your merchant; 0 = available on the platform but not enabled for your merchant |
supportedCurrency | Supported currency; B2B currently supports USDT only |
thumbnail | Complete game-thumbnail URL for display in the merchant frontend lobby |
isMultiplayer | Multiplayer flag: 0 = No / 1 = Yes |
sort | Sort order; smaller values appear first and may be used to order the merchant lobby |
This API returns all games available on the platform.
statusindicates whether the game is enabled for your merchant. Display entry points only for games withstatus=1; passinggame_codefor a game that is not enabled causes/launchto return4006.
Error codes: Signature failure, disabled merchant, or unregistered IP returns { "code": <4002|4005|4004>, "message": "SIGNATURE_VERIFY_FAILED" or corresponding text, "games": null }. Failure messages from this API are not localized; signature failure always uses the literal SIGNATURE_VERIFY_FAILED. Use code for decisions.
Call this API whenever a player enters the game lobby, or cache it for a short TTL such as five minutes. Display only games with
status=1.
13.2 Appendix: Game Code Quick Reference (Snapshot Only)
This table is a point-in-time snapshot, not a contract. The authoritative source of
game_codeis thedictLabelin the platform'sgame_typedictionary. Operations may add, remove, or rename games. Historical changes includeCoinflip→flipCoin,Xoc Dia→Xocdia, andCock fighting→CockFight. Always use the/api/v1/game/listAPI in Section 13.1. This table is only for understanding naming conventions and manual checks during integration.
| No. | Game Name (Chinese / English) | game_code |
|---|---|---|
| 1 | 爆点 / Crash | Crash |
| 2 | 骰宝 / Dice | Dice |
| 3 | 希洛 / HiLo | Hilo |
| 4 | 过纸牌 / Between | Between |
| 5 | 翻转硬币 / Flip Coin | flipCoin |
| 8 | 轮盘赌 / Roulette | Roulette |
| 9 | 哈希28 / Hash 28 | TwentyEight |
| 10 | 极速倍率 / Limbo | Limbo |
| 11 | 扫雷 / Mine | Mine |
| 12 | 视频扑克 / Video Poker | VideoPoker |
| 13 | 生肖扫雷 / Zodiac | Zodiac |
| 14 | 传奇之塔 / Tower | Tower |
| 15 | 虾蟹 / Xoc Dia | Xocdia |
| 16 | 牛牛 / Bull | Bull |
| 17 | บาคาร่า / Baccarat | Baccarat |
| 18 | บาคาร่า(变体)/ Baccarats | Baccarats |
| 19 | 博状元 / Bo Bing | BoBing |
| 20 | 斗鸡 / Cock Fighting | CockFight |
| 21 | 无佣百家乐 / No Commission Baccarat | NCBaccarat |
| 22 | 黄金峡谷 / Golden Canyon | goldenCanyon |
| 23 | 赛博朋克 / Cyberpunk | cyberpunk |
| 24 | 财神 / Fortune God | fortuneGod |
| 25 | 火龙 / Fiery Dragon | fieryDragon |
| 26 | 太空入侵者 / Space Invader | spaceInvader |
The "No." column contains the
dictValuefrom the platform'sgame_typedictionary, an internal identifier that merchants do not need to send./launchaccepts only thegame_codelabel. Numbers 6 and 7 are deprecated.Matching rules: Matching is case-insensitive, so
crash,CRASH, andCrashare equivalent. If a label contains spaces, only its first word may be sent. Other spelling differences—such as using the historical nameCoinflipinstead offlipCoin—fail to match and return4006.