API Documentation
This page contains detailed documentation for the 2J Game API, including global interface descriptions, error code definitions, amount parameters, game types, and more. By reading this page, developers can understand how to interact with 2J Game servers, how to handle different types of game requests, and how to handle API error codes.
Seamless Wallet Mode and Transfer Wallet Mode are two wallet modes provided by 2J Game. OPs can choose the appropriate mode based on their needs.
Seamless Wallet Mode
Seamless Wallet Mode refers to the seamless integration of user account balances between OP and 2J, where users do not need to perform deposits and withdrawals between OP and 2J. When players play on the 2J platform (such as betting and payout actions), 2J will operate the player's account by calling OP's interface.
Interaction Flow
Integration Documentation
Click to view Seamless Wallet Mode Integration Documentation
Transfer Wallet Mode
Transfer Wallet Mode refers to the transfer operation of user account balances between OP and 2J. OP needs to call 2J's interface to perform deposits and withdrawals for player accounts. When players play on the 2J platform (such as betting and payout actions), 2J will directly operate the player's account, and OP can query the player's account balance by calling 2J's interface.
Interaction Flow
Integration Documentation
Click to view Transfer Wallet Mode Integration Documentation
Global Interface Description
Common Parameters
Every request between OP and 2J servers must carry the following parameters in the querystring:
| Parameter | Type | Required | Description |
|---|---|---|---|
| mch | int | Yes | Identifier ID assigned by 2J to the accessing OP (usually a three-digit or four-digit ID) |
| ts | int | Yes | Timestamp in milliseconds |
| sign | string | Yes | Signature, see Signature Calculation and Verification for the rules |
| sweepstakes | string | No | Specify the currency type: sc/gc; required for merchants with SweepStakes accounts |
Amount Parameters
Amount parameters in interface interactions between OP and 2J servers are in li (厘) units, with data type int64.
1 yuan = 1000 li
1 fen = 10 liSignature Calculation and Verification
Calculate sign for each request and include it in the URL querystring alongside mch and ts. See Common Parameters for their definitions.
Calculation Rules
sign = lowercase_hex(MD5(rawBodyBytes + UTF8(ts) + UTF8(key)))+ means concatenating bytes in this order, without separators.
| Input / output | Rule |
|---|---|
jsonbody / rawBodyBytes | The original HTTP request body bytes actually sent or received, encoded in UTF-8 |
ts | The decimal string of the millisecond timestamp in the URL query; use the same value for calculation and transmission |
key | The merchant's secret key, encoded in UTF-8 for calculation and kept out of the request |
sign | The MD5 digest as a 32-character lowercase hexadecimal string |
Only the original body, ts, and key enter the digest. The URL path, mch, sweepstakes, and sign itself are not included.
The receiver calculates the signature with the received original body, the query's ts, and the corresponding merchant's key, then compares the complete result with the query's sign.
jsonbody must be the original request body
jsonbody means the original HTTP body, not a parsed JSON object or a string produced by serializing that object again.
Languages, frameworks, and serializer settings can produce different field orders, whitespace, line breaks, Unicode escapes, and number formats. For example, {"amount":1}, { "amount": 1 }, and {"amount":1.0} can represent the same business data, but their bytes and signatures differ.
Before calculating the signature, do not parse and reserialize, sort, format, minify, or trim the body. Preserve its original whitespace, line breaks, and escape sequences.
Language Examples
Each example has two purposes: calculate sign, then put it in the URL query and send a POST request. Prepare the original body once and use the same bytes for signing and sending, without JSON parsing or reserialization.
The full API URL https://api.example.com/open2j/c/create, mch = 108, and key = example_key are placeholders. Replace them with values for your test environment. ts is the current request's millisecond timestamp, so the signature changes when the body, ts, or key changes. Expand a language to view the code and save source files as UTF-8.
Go · Go 1.17+ / Standard library
In an HTTP service, use io.ReadAll(r.Body) to read the original bytes and handle read errors. Sign those bytes rather than JSON recreated from a struct.
package main
import (
"bytes"
"crypto/md5"
"encoding/hex"
"fmt"
"io"
"log"
"net/http"
"net/url"
"strconv"
"time"
)
// rawBody must be the exact bytes sent or received over HTTP.
func computeSign(rawBody []byte, ts, key string) string {
h := md5.New()
h.Write(rawBody)
h.Write([]byte(ts))
h.Write([]byte(key))
return hex.EncodeToString(h.Sum(nil))
}
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() error {
rawBody := []byte(`{"op_id":"player01","user_info":{"nickname":"Alice","gender":0,"cnt":"ph","lan":"en"}}`)
mch, key := "108", "example_key"
ts := strconv.FormatInt(time.Now().UnixMilli(), 10)
// Sign the original UTF-8 bytes; do not parse or reserialize the JSON.
sign := computeSign(rawBody, ts, key)
query := url.Values{"mch": {mch}, "ts": {ts}, "sign": {sign}}.Encode()
endpoint := "https://api.example.com/open2j/c/create?" + query
// Send the exact same bytes used above to compute the signature.
req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(rawBody))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
fmt.Printf("Status: %s\n%s\n", resp.Status, body)
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("HTTP request failed with status %d", resp.StatusCode)
}
return nil
}Java · JDK 11+ / Standard library
In a Servlet service, read the original bytes from request.getInputStream() rather than rebuilding the body from a DTO or Map. Compile with javac -encoding UTF-8 when source files contain non-ASCII characters.
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
public class SignatureExample {
// rawBody must be the exact bytes sent or received over HTTP.
public static String computeSign(byte[] rawBody, String ts, String key)
throws NoSuchAlgorithmException {
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update(rawBody);
md5.update(ts.getBytes(StandardCharsets.UTF_8));
md5.update(key.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder(32);
for (byte value : md5.digest()) {
hex.append(String.format("%02x", value & 0xff));
}
return hex.toString();
}
public static void main(String[] args) throws Exception {
byte[] rawBody = ("{\"op_id\":\"player01\",\"user_info\":{\"nickname\":\"Alice\","
+ "\"gender\":0,\"cnt\":\"ph\",\"lan\":\"en\"}}").getBytes(StandardCharsets.UTF_8);
String mch = "108";
String key = "example_key";
String ts = Long.toString(System.currentTimeMillis());
// Sign the original UTF-8 bytes; do not parse or reserialize the JSON.
String sign = computeSign(rawBody, ts, key);
String query = "mch=" + URLEncoder.encode(mch, StandardCharsets.UTF_8)
+ "&ts=" + URLEncoder.encode(ts, StandardCharsets.UTF_8)
+ "&sign=" + URLEncoder.encode(sign, StandardCharsets.UTF_8);
URI endpoint = URI.create("https://api.example.com/open2j/c/create?" + query);
HttpRequest request = HttpRequest.newBuilder(endpoint)
.timeout(Duration.ofSeconds(10))
.header("Content-Type", "application/json; charset=utf-8")
// Send the exact same bytes used above to compute the signature.
.POST(HttpRequest.BodyPublishers.ofByteArray(rawBody))
.build();
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
HttpResponse<String> response = client.send(
request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
System.out.println("Status: " + response.statusCode());
System.out.println(response.body());
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new IllegalStateException("HTTP request failed with status " + response.statusCode());
}
}
}Node.js · Node.js 18+ / ES Modules
Retain the original request body as a Buffer. With Express or similar frameworks, capture it before JSON parsing middleware; do not call JSON.stringify on parsed req.body.
import { createHash } from 'node:crypto'
import { pathToFileURL } from 'node:url'
// rawBody must be the exact Buffer sent or received over HTTP.
export function computeSign(rawBody, ts, key) {
return createHash('md5')
.update(rawBody)
.update(ts, 'utf8')
.update(key, 'utf8')
.digest('hex')
}
async function main() {
const rawBody = Buffer.from('{"op_id":"player01","user_info":{"nickname":"Alice","gender":0,"cnt":"ph","lan":"en"}}', 'utf8')
const mch = '108'
const key = 'example_key'
const ts = Date.now().toString()
const url = new URL('https://api.example.com/open2j/c/create')
url.search = new URLSearchParams({ mch, ts, sign: computeSign(rawBody, ts, key) }).toString()
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 10_000)
try {
// Node.js 18+: send the same Buffer that was signed.
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json; charset=utf-8' },
body: rawBody,
signal: controller.signal,
})
console.log(`HTTP ${response.status}`)
console.log(await response.text())
if (!response.ok) process.exitCode = 1
} finally {
clearTimeout(timeout)
}
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch(() => {
console.error('Request failed or exceeded the 10-second timeout.')
process.exitCode = 1
})
}Python 3 · Python 3.7+ / Standard library
The signature function accepts bytes. For example, Flask's request.get_data() and Django's request.body expose the raw body. Do not call json.dumps on parsed data.
import hashlib
import sys
import time
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode
from urllib.request import Request, urlopen
def compute_sign(raw_body: bytes, ts: str, key: str) -> str:
"""raw_body must be the exact bytes sent or received over HTTP."""
md5 = hashlib.md5()
md5.update(raw_body)
md5.update(ts.encode("utf-8"))
md5.update(key.encode("utf-8"))
return md5.hexdigest()
def main() -> int:
raw_body = '{"op_id":"player01","user_info":{"nickname":"Alice","gender":0,"cnt":"ph","lan":"en"}}'.encode("utf-8")
mch = "108"
key = "example_key"
ts = str(time.time_ns() // 1_000_000)
query = urlencode({"mch": mch, "ts": ts, "sign": compute_sign(raw_body, ts, key)})
url = "https://api.example.com/open2j/c/create?" + query
try:
# Send the same bytes that were signed.
request = Request(url, data=raw_body, method="POST",
headers={"Content-Type": "application/json; charset=utf-8"})
with urlopen(request, timeout=10) as response:
print(f"HTTP {response.status}")
print(response.read().decode("utf-8", errors="replace"))
return 0 if 200 <= response.status < 300 else 1
except HTTPError as error:
with error:
print(f"HTTP {error.code}")
print(error.read().decode("utf-8", errors="replace"))
return 1
except (URLError, OSError, ValueError):
print("Request failed or exceeded the 10-second timeout.", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())PHP · PHP 7+ / Built-in functions
Use file_get_contents('php://input') to read the original body and handle read failures. PHP strings are byte sequences; do not pass the body through json_decode / json_encode.
<?php
declare(strict_types=1);
// Sign the original body bytes, followed by ts and key, with no separators.
function computeSign(string $rawBody, string $ts, string $key): string
{
return md5($rawBody . $ts . $key);
}
$url = 'https://api.example.com/open2j/c/create';
$mch = '108';
$key = 'example_key';
$rawBody = '{"op_id":"player01","user_info":{"nickname":"Alice","gender":0,"cnt":"ph","lan":"en"}}';
$ts = sprintf('%.0f', floor(microtime(true) * 1000));
$sign = computeSign($rawBody, $ts, $key);
$query = http_build_query([
'mch' => $mch,
'ts' => $ts,
'sign' => $sign,
], '', '&', PHP_QUERY_RFC3986);
$context = stream_context_create(['http' => [
'method' => 'POST',
'header' => "Content-Type: application/json; charset=utf-8\r\n",
'content' => $rawBody, // Send the same bytes that computeSign hashed.
'timeout' => 10,
'ignore_errors' => true, // Read non-2xx response bodies too.
'follow_location' => 0,
]]);
$responseBody = @file_get_contents($url . '?' . $query, false, $context);
if ($responseBody === false) {
fwrite(STDERR, "HTTP request or response read failed.\n");
exit(1);
}
$status = 0;
foreach ($http_response_header as $header) {
if (preg_match('/^HTTP\/\S+\s+(\d{3})\b/', $header, $matches)) {
$status = (int) $matches[1];
}
}
echo 'HTTP ', $status, PHP_EOL, $responseBody, PHP_EOL;
if ($status < 200 || $status >= 300) {
fwrite(STDERR, "HTTP request returned a non-success status.\n");
exit(1);
}Troubleshooting
| Check | What to compare |
|---|---|
| Original body | Compare the actual bytes sent or received. Check for reserialization, reordered fields, extra spaces, or a trailing newline first. |
| Encoding | Use UTF-8 for the body, ts, and key. Literal Chinese characters and their \uXXXX escape sequences have different bytes. |
| Timestamp | Use the same ts string from the query; do not generate a new timestamp when verifying or convert milliseconds to seconds. |
| Key and concatenation | Check the merchant key and concatenate body → ts → key, without separators or other query parameters. |
| Digest format | Use 32 lowercase hexadecimal characters, retaining leading zeroes; do not use uppercase, Base64, or the binary digest. |
Signature Calculator and Verifier
The example body, ts, key, and matching sign are prefilled, so you can calculate and verify immediately. Replace them with your own original request data as needed; leave sign blank to calculate only. Calculations run in the current browser without network requests.
Response Structure
All interface response structures contain the following fields:
{
"header": {
"code": 0, // Error code, 0 means normal, non-0 means abnormal
"msg": "", // Error message
"timestamp": 1709716095516
},
}Common Interfaces
1. User Registration /open2j/c/create
Method: POST
URI: /open2j/c/create
Description: Used to register user information on the 2J server.
Request Body:
Content-Type: application/json
{
"op_id": "xxx-xxx-aaa", // OP side user's unique identifier, required
"user_info": {
"nickname": "player01", // OP player nickname. Note: emoji special characters not supported
"gender": 0, // 0-unknown, 1-female, 2-male
"cnt": "ph", // Default country
"lan": "en" // Default language
}
}Parameter description:
| Parameter | Type | Description | Note |
|---|---|---|---|
| op_id | string | Unique user identifier from OP side | Required, Up to 64 lengths |
| user_info | object | User information | Required |
| user_info.nickname | string | Player nickname from OP | Required |
| user_info.gender | int | Player gender from OP | Required |
| user_info.cnt | string | Default country | Optional |
| user_info.lan | string | Default language | Optional |
Response Body:
Content-Type: application/json
{
"header": {
"code": 0,
"trace_id": "9cd0bfd22447bf607d20a3c2ac4e304a",
"msg": "",
"timestamp": 1710310573038
}// Common response header
}2. Get Game Link /open2j/c/launch
Method: POST
URI: /open2j/c/launch
Description: Used to get game link interface.
Request Body:
Content-Type: application/json
{
"op_id": "xxx-xxx-aaa", // Required. OP side user's unique identifier
"game_id": 65005, // Required. Game ID
"lang": "en-US",
"backlink": "http://xxxxx.com?a=ss", // Return to OP address
"device_type": 2, // Optional. Player client type 1: h5, 2: android, 3: ios
"device_id": "AD19939", // Optional. Player client device unique ID
"ret_lobby_btn": false, // Optional. Whether to show return to lobby button
"full_screen": 1, // Optional. Controls whether to automatically enter fullscreen when using browser 1: don't enter fullscreen 2: automatically enter fullscreen, if not provided, controlled by 2j server
"auto_create_account": false, // Optional. Whether to automatically create an account, true: automatically create an account, false: do not automatically create an account
"keep_token": false // Optional. Whether to keep token unchanged
}Parameter description:
| Parameter | Type | Description | Note |
|---|---|---|---|
| op_id | string | Unique user identifier from OP side | Required |
| game_id | int | Game ID | Required |
| lang | string | Language | Optional |
| backlink | string | Return URL to OP | Optional |
| device_type | int | Client device type 1: h5, 2: android, 3: ios | Optional |
| device_id | string | Unique device ID of client | Optional |
| ret_lobby_btn | bool | Whether to display return to lobby button | Optional |
| full_screen | int | Control auto fullscreen in browser mode | Optional |
| auto_create_account | bool | Whether to automatically create an account | Optional |
| keep_token | bool | Whether to keep token unchanged | Optional |
- game_id detailed list can be found here
- lang reference: https://zh.wikipedia.org/wiki/ISO_639-1
- backlink: Controls the logic executed by the exit game button in the game
- If about:blank or empty is passed, triggers code:
window.close() - If a link is passed, triggers code:
window.location.href=backlink - If using Iframe to open the game, you can use setting backlink parameter as script, for example: javascript:window.parent.postMessage({ type: 'exit_customer_page'}, '*'), this will trigger the parent page's onmessage event, then the parent page can handle according to the type parameter.
- If about:blank or empty is passed, triggers code:
Response Body:
Content-Type: application/json
{
"header": {
"code": 0,
"trace_id": "9cd0bfd22447bf607d20a3c2ac4e304a",
"msg": "",
"timestamp": 1710310573038
},
"url": "http://abc.zzz?brd=default&chn=&lang=en-US&launch=blackjack&mch=108&opid=demo10087&token=6defed39fd6a4e98add31a4e1ead5172&version=1"
}3. Evict User /open2j/c/evict
Method: POST
URI: /open2j/c/evict
Description: Provided to OP for evicting users. If the player is in a game, they will be forcibly exited from the game.
Note: After being evicted, the player will receive a popup notification informing them of the eviction, but if OP did not provide a
backlinkparameter when calling/open2j/c/launch, the player will not be able to return to the OP side.
Request Body:
Content-Type: application/json
{
"op_id": "xxx-xxx-aaa", // Required. OP side user's unique identifier
}Response Body:
Content-Type: application/json
Note: If the player is in multiple games, the game information will be returned for the latest active game.
{
"header": {
"code": 0,
"trace_id": "9cd0bfd22447bf607d20a3c2ac4e304a",
"msg": "",
"timestamp": 1710310573038
},
"game_info": {
"game_id": 1006, // Game ID
"join_at": 1720686368, // Join time
"leave_at": 1720686368, // Leave time
}
}4. Detect User Gaming Status /open2j/c/detect-user-gaming
Method: POST
URI: /open2j/c/detect-user-gaming
Description: Used to detect user's gaming status.
Request Body:
Content-Type: application/json
{
"op_id": "xxx-xxx-aaa" // Required. Unique user identifier from OP side
}Response Body:
Content-Type: application/json
{
"header": {
"code": 0,
"trace_id": "9cd0bfd22447bf607d20a3c2ac4e304a",
"msg": "",
"timestamp": 1710310573038
},
"is_gaming": false, // Whether the user is in game
"game_ids": [1006] // List of game IDs
}