Download OpenAPI specification:
供娛樂城(商戶)後端串接的 HTTP API。涵蓋玩家建立、入金 / 出金、開遊戲、 拉注單、踢出與報表。所有金額以商戶幣別計(Phase 1 多為 TWD)。
轉帳錢包模式:玩家餘額存在本平台。商戶以
充值/提現將額度搬進 / 搬出本平台。
POST /v1/player/login 帶入商戶端玩家 ID(player_id),平台即建立該玩家與錢包。POST /v1/wallet/deposit 將額度搬進玩家錢包(每筆給唯一 transaction_id)。POST /v1/game/launch-url 取得帶 JWT 的 launch_url,以 iframe 嵌入或整頁導向讓玩家進場。GET /v1/player/balance 查餘額、POST /v1/game/bet-records 拉注單。POST /v1/wallet/withdraw 出金、POST /v1/player/kick 踢出在線玩家。每個請求皆須帶三個標頭:
| 標頭 | 內容 |
|---|---|
X-API-Key |
商戶 API 識別碼(非機密) |
X-Timestamp |
當前 Unix 秒;伺服器接受 ±300 秒窗口 |
X-Signature |
hex( HMAC-SHA256( body + timestamp, api_secret ) ) |
api_secret 只存在商戶後端,絕不可進前端、瀏覽器或 URL。403。X-Signature 在 600 秒內只能用一次。金流端點(deposit/withdraw)
以 transaction_id 自身做冪等,合法重送同簽章不會被誤擋。⚠️ 最常見的簽章錯誤:簽章用的 body 必須與實際送出的 HTTP body 位元組完全一致。 請先把 body 序列化成字串,同一份字串既拿去簽章也拿去送(切勿序列化兩次,兩次結果可能不同)。
🔧 線上簽章驗證工具:signature-tool.html —— 貼上 secret / body / timestamp, 在瀏覽器本機算出
X-Signature並與你後端產出的比對,快速定位不符問題(secret 不會送出)。
Node.js
import crypto from 'node:crypto';
const sign = (body, ts, secret) =>
crypto.createHmac('sha256', secret).update(body + ts).digest('hex');
const body = JSON.stringify({ player_id: 'p001', amount: 10000, transaction_id: 'dep_1' });
const ts = Math.floor(Date.now() / 1000).toString();
const headers = {
'X-API-Key': API_KEY,
'X-Timestamp': ts,
'X-Signature': sign(body, ts, API_SECRET),
'Content-Type': 'application/json',
};
// 送出時用同一個 body 變數:fetch(url, { method: 'POST', headers, body })
PHP
<?php
function sign(string $body, string $ts, string $secret): string {
return hash_hmac('sha256', $body . $ts, $secret);
}
$body = json_encode(['player_id' => 'p001', 'amount' => 10000, 'transaction_id' => 'dep_1']);
$ts = (string) time();
$sig = sign($body, $ts, $apiSecret);
$ch = curl_init("$base/v1/wallet/deposit");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"X-API-Key: $apiKey",
"X-Timestamp: $ts",
"X-Signature: $sig",
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => $body, // 與簽章同一份字串
CURLOPT_RETURNTRANSFER => true,
]);
$resp = curl_exec($ch);
Golang
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"time"
)
func sign(body, ts, secret string) string {
m := hmac.New(sha256.New, []byte(secret))
m.Write([]byte(body + ts))
return hex.EncodeToString(m.Sum(nil))
}
body := `{"player_id":"p001","amount":10000,"transaction_id":"dep_1"}`
ts := fmt.Sprintf("%d", time.Now().Unix())
sig := sign(body, ts, apiSecret)
// 設 header:X-API-Key / X-Timestamp=ts / X-Signature=sig;送出時用同一份 body 位元組
bash + openssl
sign() { printf '%s%s' "$1" "$2" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //'; }
ts=$(date +%s); sig=$(sign "$BODY" "$ts")
curl -X POST "$BASE/v1/wallet/deposit" \
-H "X-API-Key: $KEY" -H "X-Timestamp: $ts" -H "X-Signature: $sig" \
-H "Content-Type: application/json" -d "$BODY"
錯誤回應格式:{ "success": false, "code": <http_status>, "message": "<說明>" }
| HTTP | 情境 |
|---|---|
400 |
缺必填欄位 / 金額非正數(如 player_id is required、amount must be positive) |
401 |
invalid signature(簽錯)/ timestamp expired(時戳超窗)/ duplicate request(重放) |
403 |
IP not in whitelist(來源 IP 未列入白名單) |
404 |
操作 / 踢出不存在或跨商戶玩家(不洩漏存在性) |
405 |
HTTP 方法不允許 |
500 |
伺服器內部錯誤 |
502 |
上游遊戲伺服器無法連線(踢出時) |
首次帶某 player_id 呼叫即在本平台建立該玩家(含錢包)。冪等:重複呼叫回同一內部 ID。
| player_id required | string 商戶端玩家 ID(external,由商戶自訂;同商戶內唯一) |
| nickname | string 玩家暱稱(顯示用;可省略) |
{- "player_id": "p001",
- "nickname": "玩家暱稱"
}{- "success": true,
- "internal_player_id": 229
}以商戶端玩家 ID 查詢餘額。available = balance − frozen。查無此玩家回 0(不建立資料)。
| player_id required | string Example: player_id=p001 |
{- "balance": 7000,
- "frozen": 0,
- "available": 7000,
- "currency": "TWD"
}踢出自家玩家並使其當前 session 失效。
獲取遊戲登錄地址 取得新 launch_url。kicked=false 表玩家本就不在線(冪等成功)。404(不洩漏跨商戶存在性)。| player_id required | string 商戶端玩家 ID(external) |
| reason | string 踢出原因(記錄用;可省略) |
{- "player_id": "p001",
- "reason": "compliance"
}{- "success": true,
- "kicked": true
}將額度從商戶搬進本平台玩家錢包。transaction_id 為商戶側唯一交易號(冪等鍵)。
同 transaction_id 重送(含併發 / 逾時重試)一律回原結果(200),不會重複入帳。
| player_id required | string 商戶端玩家 ID(external) |
| amount required | number 金額(正數;最多 2 位小數;上限 10000000)。非正數回 400。 |
| transaction_id required | string 商戶側唯一交易號(冪等鍵)。同值重送回原結果,不重複入帳/扣款。 |
{- "player_id": "p001",
- "amount": 10000,
- "transaction_id": "dep_p001_001"
}{- "success": true,
- "internal_transaction_id": 178,
- "balance_after": 10000
}將額度從本平台玩家錢包搬回商戶。餘額不足回 400 insufficient balance。同 transaction_id 冪等。
| player_id required | string 商戶端玩家 ID(external) |
| amount required | number 金額(正數;最多 2 位小數;上限 10000000)。非正數回 400。 |
| transaction_id required | string 商戶側唯一交易號(冪等鍵)。同值重送回原結果,不重複入帳/扣款。 |
{- "player_id": "p001",
- "amount": 3000,
- "transaction_id": "wd_p001_001"
}{- "success": true,
- "internal_transaction_id": 179,
- "balance_after": 7000
}回傳帶 JWT 的 launch_url。商戶可 iframe 嵌入或整頁導向讓玩家進場。
game_type 決定:不帶 → 選遊戲大廳;帶值 → 直接進該遊戲(詳見 game_type 欄位)。enabled_games 決定(後台管理),非每次 launch 帶入。return_url 簽進 JWT(前端只信 token 內值,防釣魚轉址)。launch_url(舊 token 已失效)。| player_id required | string 商戶端玩家 ID(external) |
| nickname | string 玩家暱稱(顯示用) |
| game_type | string Enum: "texas" "mahjong" "bigtwo" "thirteen" "fantan" "darkchess" 選填 — 決定玩家進場「落點」(非用來控制商戶開放哪些遊戲;開放清單見商戶設定
可用值(enum):
|
| return_url | string 玩家退出遊戲後導回娛樂城的網址(簽入 JWT,前端只信 token 內值,防釣魚轉址) |
| lang | string 語系(如 zh-TW / en);空則前端用預設 |
{- "player_id": "p001",
- "nickname": "玩家暱稱",
- "game_type": "texas",
- "lang": "zh-TW"
}{- "success": true,
- "launch_url": "https://play.example.com/?token=eyJhbGciOiJIUzI1Ni... ",
- "token": "eyJhbGciOiJIUzI1Ni...",
- "expires_at": 1782593360,
- "session_expires_at": 1782611360
}查自家玩家逐手注單(投注 / 輸贏 / 抽水 / 有效投注)。
player_id 留空 = 該商戶全部玩家。page_size 預設 20、上限 100;排序 created_at DESC(最新在前)。net_amount 加總 + 抽水 = 0(零和)。| player_id | string 商戶端玩家 ID;留空 = 該商戶全部玩家 |
| start_time | integer <int64> 起始時間 Unix 秒(含);0 = 不限 |
| end_time | integer <int64> 結束時間 Unix 秒(含);0 = 不限 |
| page | integer Default: 1 頁碼(1 起) |
| page_size | integer <= 100 Default: 20 每頁筆數(預設 20,上限 100) |
{- "player_id": "p001",
- "start_time": 0,
- "end_time": 0,
- "page": 1,
- "page_size": 20
}{- "success": true,
- "total": 72,
- "page": 1,
- "page_size": 5,
- "records": [
- {
- "round_id": 13,
- "player_id": 225,
- "seat_idx": 2,
- "is_winner": true,
- "bet_amount": 5000,
- "win_amount": 25,
- "net_amount": 25,
- "rake_amount": 0,
- "valid_bet": 25,
- "created_at": "2026-06-27T19:21:42Z"
}
]
}依日期區間查詢每日統計。data 為日報陣列;無資料時為空陣列。金額欄位為字串(保留精度)。
| start_date required | string <date> Example: start_date=2026-06-01 起始日期(含),格式 YYYY-MM-DD |
| end_date required | string <date> Example: end_date=2026-06-28 結束日期(含),格式 YYYY-MM-DD |
{- "success": true,
- "data": [
- {
- "id": 1,
- "merchant_id": 2,
- "date": "2026-06-28T00:00:00Z",
- "new_players": 5,
- "active_players": 20,
- "round_count": 120,
- "bet_amount": "600000",
- "valid_bet": "580000",
- "win_amount": "595000",
- "net_revenue": "5000",
- "created_at": "2026-06-28T01:00:00Z"
}
]
}查詢指定年度的每週統計。data 為週報陣列。
| year required | integer Example: year=2026 年度(西元年) |
{- "success": true,
- "data": [
- {
- "id": 1,
- "merchant_id": 2,
- "year": 2026,
- "week": 26,
- "start_date": "2026-06-22T00:00:00Z",
- "end_date": "2026-06-28T00:00:00Z",
- "new_players": 30,
- "active_players": 95,
- "round_count": 820,
- "bet_amount": "4100000",
- "valid_bet": "3980000",
- "win_amount": "4060000",
- "net_revenue": "40000",
- "created_at": "2026-06-29T01:00:00Z"
}
]
}查詢指定年度的每月統計。data 為月報陣列。
| year required | integer Example: year=2026 年度(西元年) |
{- "success": true,
- "data": [
- {
- "id": 1,
- "merchant_id": 2,
- "year": 2026,
- "month": 6,
- "new_players": 120,
- "active_players": 310,
- "round_count": 3500,
- "bet_amount": "17500000",
- "valid_bet": "17000000",
- "win_amount": "17300000",
- "net_revenue": "200000",
- "created_at": "2026-07-01T01:00:00Z"
}
]
}查詢指定日期的逐小時即時統計。date 必填(未帶回 400 date is required)。data 為各小時統計陣列。
| date required | string <date> Example: date=2026-06-28 日期(必填),格式 YYYY-MM-DD |
{- "success": true,
- "data": [
- {
- "id": 1,
- "merchant_id": 2,
- "date": "2026-06-28T00:00:00Z",
- "hour": 14,
- "player_count": 12,
- "round_count": 30,
- "bet_amount": "150000",
- "win_amount": "148000",
- "updated_at": "2026-06-28T14:30:00Z"
}
]
}