HashPlay sandbox is open — Request credentials
Developers

Developer docs

Hash Play Merchant Integration Guide



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.

ActionDirectionDescription
Launch a gameYour platform → Hash PlayAfter 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 operationsYour platform → Hash PlayThe merchant calls deposit, withdrawal, balance, transaction, order, and operation-status APIs as needed to manage the player's hosted wallet
In-game settlementInternal to Hash PlayBets, 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/deposit or /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:

ConfigurationSandboxProduction
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/gamehttps://api.example.com/prod-api/customer/app-api/api/v1/game
API Versionv1 (/api/v1/game/launch, /api/v1/game/list, /api/v1/game/wallet/*)Same as Sandbox
Test operator_idAssigned by Hash Play (example: M001)Official merchant code
Test api_key / api_secretAssigned by Hash PlayProduction credentials are issued separately and must never be mixed with Sandbox credentials
Test gamesDice (the Dice game code and name are identical), etc. See Chapter 13: Game ListEnabled according to the merchant's contracted game scope
Test currencyUSDT (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 registeredProvided 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 windowReusing the same random string within approximately two minutes is treated as a replay and rejectedSame 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 route customer, application prefix app-api, and api/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/status

Always 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

CredentialPurpose
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:

ConfigurationDescription
Merchant server egress IPProvided 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 as 203.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:

  1. The request source is trusted and has not been forged by a third party.
  2. The content has not been modified, including amount parameters in transit.
  3. 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:

HeaderMeaningDescription
X-API-KeyMerchant identifierSet to the merchant API Key
X-TimestampRequest timestampAlways a millisecond timestamp (13-digit Unix epoch milliseconds). Requests with a server-time difference greater than ±60 seconds (60000 ms) are rejected
X-NonceOne-time random stringUnique 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-SignatureSignatureCalculated 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.

  1. Take all top-level fields from the JSON request body and remove fields with empty values.
  2. Sort by field name in lexicographic order, then concatenate as field=value&field=value&....
  3. Append the timestamp and random string in that order, separated by &.

For example, given this deposit request body:

json
{
  "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:

code
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

  1. The timestamp unit is always milliseconds: X-Timestamp uses 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.
  2. Never transmit amounts as JSON numbers: Fields such as amount, balance, bet_amount, win_amount, and mult_value are 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 to 10.0, causing a mismatch with the merchant signature string and a 4002 response. When building the canonical string, concatenate the request-body value exactly as provided ("1000.00"amount=1000.00) without numeric formatting. Convert it with Number() only for frontend display.
  3. Remove empty values: Fields whose values are null or empty strings do not participate in canonical-string construction. The platform follows the same rule, so "amount": null is equivalent to omitting the field.
  4. Field names are case-sensitive: Canonical-string and request-body field names use lowercase snake case, such as user_id, request_id, and game_code. Do not use camel case.

5.4 Standard Signature-Verification Failure Response

If Hash Play cannot verify a merchant request signature, it returns:

json
{
  "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):

json
{ "code": 0, "message": "success", "...": "各接口自有字段" }

Be aware that the two error categories use different field names. Your parser must support both:

Error sourceResponse shapeDescription
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: /launch and /exchange apply annotation-based validation to the request body. Omitting required fields such as operator_id, user_id, or currency produces 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 as 4011. 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 with data.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 '&timestamp&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

  1. The player selects a game on the merchant platform.
  2. 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.
  3. 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.
  4. 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: /launch finds 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 returned game_url must be opened separately.

Balance source: The returned balance is the player's current platform-hosted balance as a string. It is usually "0" on first launch. Call /wallet/deposit afterward 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 stringTreat 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_code so that permission validation runs. If game_code is omitted, the platform separately determines whether the player may start a selected game after entering the lobby.

game_code is matched case-insensitively against the label (dictLabel) in the game_type dictionary; crash, CRASH, and Crash are equivalent. If a label contains spaces, its first word may be used as shorthand. The safest approach is always to use the exact gameCode value returned by the Game List API, without modifying it.

7.2 API Definition

Merchant request:

code
POST {base_url}/api/v1/game/launch

Request parameters (JSON with lowercase snake-case field names):

ParameterRequiredVisible toDescription
operator_idYesSystemMerchant code, for example M001
user_idYesSystemUnique 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_codeNoSystemGame 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
currencyYesSystemPlayer 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
usernameNoPlayerPlayer nickname. Displayed in the game when provided; otherwise, the system default is shown
avatarNoPlayerURL of the player's avatar image. Displayed in the game when provided
languageNoPlayerGame 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_urlNoPlayerMerchant-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):

LanguageCodeLanguageCodeLanguageCode
Simplified Chinesezh-CNTraditional Chinese (Taiwan)zh-TWTraditional Chinese (Hong Kong)zh-HK
English (United States)en-USEnglish (United Kingdom)en-GBSpanish (Spain)es-ES
Japaneseja-JPKoreanko-KRVietnamesevi-VN
Thaith-THIndonesianid-IDRussianru-RU
Turkishtr-TRArabicar-SAHindihi-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:

json
{
  "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:

json
{
  "code": 0,
  "message": "success",
  "game_url": "https://game.hashplay.io/hashWeb/#/zh-CN?ticket=8f3e...c21a",
  "balance": "1000.00"
}
FieldDescription
game_urlSingle-use entry URL to open in the player's browser
balancePlayer's current platform-hosted balance as a string, such as "1000.00"; usually "0" on first launch

Failure response ({code, message, game_url: null}):

CodeMeaningMerchant action
0SuccessSend 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
4002Signature verification failedCheck the signature implementation (see Section 5.3). Do not retry until corrected
4004IP is not allowlistedThe merchant egress IP is unregistered or has changed. Contact Hash Play to verify it; do not retry
4005Merchant is invalid or disabledVerify operator_id and merchant status, then contact Hash Play
4006Game does not exist or merchant lacks permissiongame_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
4008Game limits are not configuredLimits are missing for the merchant-game combination. Contact Hash Play to enable them
4009Game URL is missing or disabledThe game entry URL is not configured. Contact Hash Play
4010Player is disabledThe platform user associated with user_id is disabled or deleted. Notify the player according to your business process
500System errorUse 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

APIMethodWhen to callDescription
Deposit /wallet/depositPOSTBefore or after game entry, when the merchant decides to fund the playerAdds amount to the hosted balance and writes a DEPOSIT transaction
Withdraw /wallet/withdrawPOSTWhen the player exits or the merchant recovers fundsWithdraws a specified amount or the entire balance and writes a WITHDRAW transaction
Balance /wallet/balancePOSTAt any time, such as balance display or risk-control checksReturns the player's current hosted balance
Transactions /wallet/transactionsPOSTFor reconciliation or customer-support investigationsQueries all player fund movements in a time range, including in-game bets, payouts, and rollbacks
Orders /wallet/ordersPOSTFor reconciliation, customer-support investigations, or betting analysisQueries player betting orders in a time range, including game code, bet/payout amount, settlement status, multiplier, and round details
Operation status /wallet/operation/statusPOSTAfter a deposit/withdrawal timeout or unknown resultDetermines 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:

code
POST {base_url}/wallet/deposit

Request parameters:

ParameterRequiredDescription
user_idYesPlayer account identifier on the merchant platform; must match the value sent to /launch
request_idYesMerchant 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
amountYesDeposit amount as a positive string with two decimal places, such as "1000.00"

request_id is 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_id is not a random string: it must uniquely identify a business transaction. Reuse the same request_id when resending after a network timeout. Use a new value for every new deposit; otherwise, the request is rejected with 4014.

Request example:

json
{
  "user_id": "u_10086",
  "request_id": "dep_20260908_0001",
  "amount": "1000.00"
}

Hash Play response:

json
{
  "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:

CodeMeaningMerchant action
4001Invalid amountamount is missing or ≤ 0. Correct the parameter and retry
4011Empty user_idAdd the parameter and retry
4012Invalid user_idNo user exists for (operator_id, user_id). Call /launch first to create the user
4013Empty request_idAdd the parameter and retry
4014This request_id has already succeededDo not execute it again. Verify with /wallet/operation/status, or use a new request_id for a new transaction
4015Wallet operation failedThe failure was recorded with status=0; retry with the original request_id

8.3 Withdraw—Recover Funds

Merchant request:

code
POST {base_url}/wallet/withdraw

Request parameters:

ParameterRequiredDescription
user_idYesPlayer account identifier on the merchant platform; must match the value sent to /launch
request_idYesMerchant order number uniquely identifying this withdrawal; a format such as wd_date_sequence is recommended. The platform enforces idempotency by (operator_id, request_id)
amountNoWithdrawal 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 amount or 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 returns 4001.

Request example (partial withdrawal):

json
{
  "user_id": "u_10086",
  "request_id": "wd_20260908_0001",
  "amount": "500.00"
}

Request example (full withdrawal):

json
{
  "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" } (amount is the withdrawn amount and balance is the latest balance after withdrawal; for a full withdrawal, balance is "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 amount to 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 returns 4001 for an invalid amount.

Possible withdraw error codes:

CodeMeaningMerchant action
4001Invalid amountamount is ≤ 0 or exceeds the current hosted balance, which may have changed because of concurrent settlement. Consider omitting amount to withdraw the full balance
4003Hosted balance is zeroNo funds are available; handle according to the business scenario, normally as already fully withdrawn
4011 / 4012Empty / invalid user_idAdd the parameter or call /launch first to create the user
4013Empty request_idAdd the parameter and retry
4014This request_id has already succeededVerify with /wallet/operation/status, or use a new request_id for a new transaction
4015Wallet operation failedThe failure was recorded with status=0; retry with the original request_id

8.4 Balance

Merchant request:

code
POST {base_url}/wallet/balance

Request parameters:

json
{ "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:

json
{
  "code": 0,
  "message": "success",
  "currency": "USDT",
  "balance": "1015.00"
}

balance is a string, such as "1015.00". Convert it with Number() or BigDecimal before displaying it in the merchant frontend. Do not use typeof === '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:

code
POST {base_url}/wallet/transactions

Request parameters:

ParameterRequiredDescription
user_idNoPlayer account identifier on the merchant platform; omit it to query transactions for all merchant users
start_timeNoStart time as an inclusive millisecond timestamp
end_timeNoEnd time as an inclusive millisecond timestamp; must not be earlier than start_time
page_numNoPage number starting from 1; default: 1
page_sizeNoRecords per page; default: 10, maximum: 500

Hash Play response:

json
{
  "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
    }
  ]
}
FieldDescription
page_num / page_size / totalPage number, records per page, and total record count
user_idPlayer account identifier on the merchant platform; it is the requested user_id, or identifies ownership in a cross-user query
txn_idGlobally unique transaction ID
txn_typeTransaction type: DEPOSIT (merchant deposit), WITHDRAW (merchant withdrawal), BET, WIN, ROLLBACK (abnormal-round refund), or another in-game fund movement
amountPositive changed amount as a string
balance_before / balance_afterBalances before and after the movement, as strings
round_idAssociated game round ID; null for deposit and withdrawal transactions
ref_txn_idOriginal related transaction ID, such as the bet corresponding to a payout or refund
remarkAdditional information
create_timeTransaction time as a millisecond timestamp

8.6 Orders—Betting Order Inspection

Merchant request:

code
POST {base_url}/wallet/orders

Request parameters (identical to the transaction query in Section 8.5):

ParameterRequiredDescription
user_idNoPlayer account identifier on the merchant platform; omit it to query orders for all merchant users
start_timeNoStart time as an inclusive millisecond timestamp
end_timeNoEnd time as an inclusive millisecond timestamp; must not be earlier than start_time
page_numNoPage number starting from 1; default: 1
page_sizeNoRecords per page; default: 10, maximum: 500

Hash Play response:

json
{
  "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": "{...}"
    }
  ]
}
FieldDescription
page_num / page_size / totalPage number, records per page, and total record count
user_idPlayer account identifier on the merchant platform; it is the requested user_id, or identifies ownership in a cross-user query
round_idGame round ID; corresponds to round_id in /wallet/transactions and supports cross-checking
game_codeGame code, consistent with /launch and the Game List API
currencyCurrency
bet_amountTotal bet amount for the round as a string
win_amountPayout amount for the round as a string
statusRound status: OPEN, SETTLED, or CANCELLED
settle_resultSettlement result: 1 for win / 0 for loss; present only for SETTLED rounds
mult_valueSettlement multiplier as a string, such as "2.5"; depends on game rules and may be empty
settle_timeSettlement time as a millisecond timestamp
create_timeRound creation time as a millisecond timestamp
game_detailGame-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_id and together provide a complete reconstruction of player activity and fund movements.

8.7 Operation Status—Verify Whether a Merchant Order Succeeded

Merchant request:

code
POST {base_url}/wallet/operation/status

Request parameters:

ParameterRequiredDescription
request_idYesMerchant order number, identical to the request_id supplied for deposit/withdraw
operation_typeYesDEPOSIT or WITHDRAW; case-insensitive. Other values return 4017

Request example:

json
{
  "request_id": "dep_20260908_0001",
  "operation_type": "DEPOSIT"
}

This API does not require user_id. A request_id unique within the merchant scope is sufficient.

Hash Play response:

json
{
  "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
}
FieldDescription
status1 for success / 0 for failure. If the same request_id has ever succeeded, the successful record takes precedence
amountOperation amount as a string; for a full withdrawal, this is the actual withdrawn amount
txn_idAssociated wallet transaction ID on success; null on failure
error_msgFailure reason; null on success
create_timeRecord 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=1 means the credit/debit succeeded; do not resend the same request_id. If status=0 or 4016 is returned, retry with the original request_id.

8.8 Standard Response Codes

CodeMeaningMerchant action
0SuccessIncludes the latest balance, and for fund movements, amount and txn_id
4001Invalid amountDeposit: missing amount or amount ≤ 0. Withdraw: amount exceeds the current balance. Correct and retry
4002Signature verification failedCheck the signature implementation and Section 5.3
4003Hosted balance is zeroReturned only by withdraw. No funds are available; handle as a business result
4004IP is not allowlistedMerchant egress IP is unregistered. Contact Hash Play
4005Merchant is invalid or disabledVerify the API Key and merchant status, then contact Hash Play
4006Invalid time rangeReturned only by transactions/orders when start_time > end_time or a timestamp is not a millisecond numeric value
4011Empty user_idAdd the parameter and retry
4012Invalid user_idNo user exists for (operator_id, user_id). Call /launch first
4013Empty request_idAdd the parameter and retry
4014request_id already succeededDo not execute again. Verify with the status API or use a new order number
4015Wallet operation failedThe failure was recorded; retry with the original request_id
4016Wallet operation record not foundOnly for /wallet/operation/status: no matching order number/type record
4017Invalid operation_typeOnly for /wallet/operation/status: only DEPOSIT / WITHDRAW are supported

Same code, different meaning: 4006, 4011, 4012, and 4013 have different meanings under /launch and /wallet/* (ticket/game-permission errors in the former and parameter errors in the latter), and some messages are localized. Evaluate the API and code together; never use code alone:

codeMeaning under /launchMeaning under /wallet/*
4006Merchant lacks game permission (see Section 7.2)Invalid transactions/orders time range
4011Empty ticket (only /exchange)Empty user_id
4012Invalid or expired ticketInvalid user_id (user not created)
4013User for ticket does 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):

code
所有存入(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:

  1. Hash Play transactions are authoritative: All in-game fund movements occur on the platform. Merchant finance teams must reconcile with /wallet/transactions plus /wallet/balance, using the formula in Chapter 9.
  2. Keep user_id stable 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.
  3. 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.
  4. Partial withdrawals are supported: Provide amount to withdraw a specified amount; omit it to withdraw the full balance and set it to zero. 4003 means the balance is already zero and is a normal business result, not a system failure.
  5. 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.
  6. Sign before calling: Every request must contain a valid signature according to Chapter 5; failures return 4002.
  7. 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.
  8. Maintain an accurate clock: Synchronize servers with NTP. See Section 5.3 for timestamp and window rules.
  9. Use the correct amount type: Never transmit amounts as JSON numbers. See Section 5.3.
  10. Persist request_id and retry safely: Persist the deposit/withdraw request_id as an idempotency key, following Section 8.2. After a timeout or unknown result, query /wallet/operation/status first. 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 4004 if it is not registered.
  • Preparation: Call /api/v1/game/list to 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 /launch and distribute game_url to 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/status after 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), invalid user_id (4012), invalid amount (4001), missing request_id (4013), duplicate request_id (4014).
  • Self-test: Send a timestamp outside the ±60-second window, verify a 4002 response, 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:

  1. Timestamp: It must be a 13-digit millisecond value within 60 seconds of Hash Play time; NTP is recommended.
  2. Signature string: Verify lexicographic field order, empty-value removal, and the trailing &{timestamp}&{nonce}.
  3. Amount type: Never use JSON numbers; send strings. See Section 5.3.
  4. Signature case: Hexadecimal output must be lowercase.
  5. Nonce: Do not reuse it within approximately two minutes.
  6. GET without a request body, such as /list: The signature string becomes &{timestamp}&{nonce}.

Test against the examples in Chapter 6.


13. Game List

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:

code
GET {base_url}/api/v1/game/list?language=zh-CN
ParameterRequiredDescription
languageNoLocale 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 &timestamp&nonce. See Section 5.2.

Hash Play response:

json
{
  "code": 0,
  "message": "success",
  "games": [
    {
      "gameCode": "Crash",
      "gameName": "爆点",
      "status": 1,
      "supportedCurrency": "USDT",
      "thumbnail": "https://resource.hashplay.io/xxx.png",
      "isMultiplayer": 0,
      "sort": 1
    }
  ]
}
FieldDescription
gameCodeGame 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
gameNameLocalized game name for language; falls back to English if no translation exists
status1 = enabled for your merchant; 0 = available on the platform but not enabled for your merchant
supportedCurrencySupported currency; B2B currently supports USDT only
thumbnailComplete game-thumbnail URL for display in the merchant frontend lobby
isMultiplayerMultiplayer flag: 0 = No / 1 = Yes
sortSort order; smaller values appear first and may be used to order the merchant lobby

This API returns all games available on the platform. status indicates whether the game is enabled for your merchant. Display entry points only for games with status=1; passing game_code for a game that is not enabled causes /launch to return 4006.

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_code is the dictLabel in the platform's game_type dictionary. Operations may add, remove, or rename games. Historical changes include CoinflipflipCoin, Xoc DiaXocdia, and Cock fightingCockFight. Always use the /api/v1/game/list API 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爆点 / CrashCrash
2骰宝 / DiceDice
3希洛 / HiLoHilo
4过纸牌 / BetweenBetween
5翻转硬币 / Flip CoinflipCoin
8轮盘赌 / RouletteRoulette
9哈希28 / Hash 28TwentyEight
10极速倍率 / LimboLimbo
11扫雷 / MineMine
12视频扑克 / Video PokerVideoPoker
13生肖扫雷 / ZodiacZodiac
14传奇之塔 / TowerTower
15虾蟹 / Xoc DiaXocdia
16牛牛 / BullBull
17บาคาร่า / BaccaratBaccarat
18บาคาร่า(变体)/ BaccaratsBaccarats
19博状元 / Bo BingBoBing
20斗鸡 / Cock FightingCockFight
21无佣百家乐 / No Commission BaccaratNCBaccarat
22黄金峡谷 / Golden CanyongoldenCanyon
23赛博朋克 / Cyberpunkcyberpunk
24财神 / Fortune GodfortuneGod
25火龙 / Fiery DragonfieryDragon
26太空入侵者 / Space InvaderspaceInvader

The "No." column contains the dictValue from the platform's game_type dictionary, an internal identifier that merchants do not need to send. /launch accepts only the game_code label. Numbers 6 and 7 are deprecated.

Matching rules: Matching is case-insensitive, so crash, CRASH, and Crash are equivalent. If a label contains spaces, only its first word may be sent. Other spelling differences—such as using the historical name Coinflip instead of flipCoin—fail to match and return 4006.