概览
基础地址https://api.fortunex.bet
第 1 版基础路径/api/b2b/v1
API 文档版本1.0.0
OpenAPI3.0.3
签名请求头X-FX-Signature
下载
供您的开发人员使用。与 FortuneX 工作人员发给新运营商的文件相同。
机器可读的接口描述为同目录下的 operator-openapi.yaml(OpenAPI 3.0)。本指南说明各部分如何配合使用。
1. 本 API 的用途
您可以通过 FortuneX 为您的玩家下 4D 投注,并读取 结果、余额 和 每周对账单。
- 您的投注通过 FortuneX 的唯一主账户发送给 4D 供应商。您无需在供应商处开设账户。
- 4D 投注 不会 调用您的钱包。您与 FortuneX 的关系是 预付余额 或 信用账期 之一(第 8 节)。
- FortuneX 对 4D 不自动收取任何费用。每一笔投注和每一个结果都会被记录,以便您和 FortuneX 与供应商的记录核对。每笔投注都带有您自己的
ref 和 subAccount 用于核对。 - 结果只是 记录,不派彩。中奖本身不会自动转账(第 9 节)。
- 没有 Webhook。您需要 轮询 状态和结果。
2. 获取访问权限
FortuneX 工作人员在管理后台为您创建运营商账户和 API 密钥。每个密钥您将获得:
- 密钥 ID(
fxk_...),公开信息; - 密钥(secret)(
fxs_...),只显示一次。FortuneX 加密保存,无法再次显示。如果遗失,请申请更换密钥。 - IP 白名单:密钥只能在您提供给 FortuneX 的地址上使用。请告知 FortuneX 您所有的调用地址,包括备用服务器。
您可以另外申请一个 沙盒密钥(第 11 节)用于测试。沙盒密钥不会连到供应商,也不会产生任何资金变动。正式密钥和沙盒密钥是不同的密钥,互相不能替代。
密钥更换:FortuneX 创建新密钥,旧密钥在约定的重叠期内继续有效(默认 7 天),之后失效。工作人员也可以立即吊销密钥。
API 主机为 https://api.fortunex.bet(最终主机以 FortuneX 通知为准)。在 FortuneX 开启运营商 API 之前,所有路径都返回 404 not_found。
3. 为每个请求签名
每个请求(GET 或 POST)都带有四个请求头:
| 请求头 | 值 |
|---|
X-FX-Key | 您的密钥 ID |
X-FX-Timestamp | 当前 Unix 秒。必须在 FortuneX 时钟的 5 分钟以内。 |
X-FX-Nonce | 8 到 128 个字符,取自 A-Z a-z 0-9 _ - + = . :,每个请求都要新生成。每个 nonce 只能使用一次。 |
X-FX-Signature | HMAC-SHA256(secret, stringToSign) 的小写十六进制结果 |
stringToSign 由六行组成,用单个换行符(\n)连接,末尾没有换行:
METHOD upper case: GET or POST
path the request path, exactly as sent, without the query
query the raw query string without the "?"; an empty line if there is none
timestamp the same value as X-FX-Timestamp
nonce the same value as X-FX-Nonce
sha256(body) lower-case hex SHA-256 of the raw body bytes; of the empty string for a GET
即:第 1 行请求方法(大写 GET 或 POST);第 2 行请求路径(与实际发送完全一致,不含查询串);第 3 行原始查询串(不含 "?",没有则为空行);第 4 行时间戳(与 X-FX-Timestamp 相同);第 5 行 nonce(与 X-FX-Nonce 相同);第 6 行请求体原始字节的 SHA-256 小写十六进制(GET 请求为空字符串的哈希)。
容易出错的地方:
- 对您 实际发送的原始字节 签名。签名后不要重新序列化 JSON。不要对路径或查询串做解码、重排或规范化。
- 路径或查询串中含有换行符的请求会被拒绝。
- 没有查询串的请求,第三行为空,所以字符串中会出现连续两个换行符。
- 请求体使用
Content-Type: application/json 发送。请求体最大 64 KB。
完整示例
密钥(仅为示例,切勿使用):fxs_example_secret_never_use_in_production
请求体(必须与下面的字节完全一致,不要多加空格):
{"ref":"order-1001","subAccount":"site-A-user-77","drawDate":"2026-11-04","pools":["magnum"],"lines":["1234#1"]}
method POST
path /api/b2b/v1/4d/bets
query (empty)
timestamp 1793700000
nonce 0f8e7d6c5b4a3921
sha256(body) = 86b0934218ce3411d21dff00918892f65ae64e1d2ba484d162736827178886ff
待签名字符串:
POST
/api/b2b/v1/4d/bets
1793700000
0f8e7d6c5b4a3921
86b0934218ce3411d21dff00918892f65ae64e1d2ba484d162736827178886ff
签名:5e55fd7bfac0380e225fdc9443f20be43d1e1018323b968263e21a3142981147
带查询串的 GET 示例:GET /api/b2b/v1/4d/bets?drawDate=2026-11-04&limit=20,时间戳 1793700001,nonce 1a2b3c4d5e6f7081,请求体为空,使用同一密钥得到的签名为 53616fcf295cab5289249468d3c92d599592d577ed3d5f9d62293b67350780e1。请用这两组数据作为您实现的测试向量。两组向量中的 nonce(16 位十六进制字符)是人工固定的,目的是让签名可以复现:验证您的代码时,请使用向量给出的时间戳和 nonce,而不是自行生成。实际调用时必须每次生成新的 nonce,如下面的代码所做的那样(24 位十六进制字符)。
代码
Node.js(无依赖):
const crypto = require('crypto');
function signedFetch(base, keyId, secret, method, path, query, bodyObj) {
const body = bodyObj == null ? '' : JSON.stringify(bodyObj);
const ts = String(Math.floor(Date.now() / 1000)), nonce = crypto.randomBytes(12).toString('hex');
const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
const toSign = [method, path, query || '', ts, nonce, bodyHash].join('\n');
const sig = crypto.createHmac('sha256', secret).update(toSign).digest('hex');
return fetch(base + path + (query ? '?' + query : ''), {
method, body: method === 'GET' ? undefined : body,
headers: { 'Content-Type': 'application/json', 'X-FX-Key': keyId, 'X-FX-Timestamp': ts, 'X-FX-Nonce': nonce, 'X-FX-Signature': sig },
});
}
Python 3:
import hashlib, hmac, json, os, time, urllib.request
def signed(base, key_id, secret, method, path, query='', body_obj=None):
body = b'' if body_obj is None else json.dumps(body_obj, separators=(',', ':')).encode()
ts, nonce = str(int(time.time())), os.urandom(12).hex()
to_sign = '\n'.join([method, path, query, ts, nonce, hashlib.sha256(body).hexdigest()])
sig = hmac.new(secret.encode(), to_sign.encode(), hashlib.sha256).hexdigest()
req = urllib.request.Request(base + path + ('?' + query if query else ''), data=body or None, method=method,
headers={'Content-Type': 'application/json', 'X-FX-Key': key_id, 'X-FX-Timestamp': ts, 'X-FX-Nonce': nonce, 'X-FX-Signature': sig})
return urllib.request.urlopen(req)
4. 响应与错误
成功:{ "ok": true, ... }。失败:{ "ok": false, "error": { "code": "...", "message": "..." } },并带有下表的 HTTP 状态码。请始终根据 code 判断,不要根据 message 文本判断。
认证失败统一返回刻意模糊的 401 unauthorized(密钥错误、签名错误、密钥不存在看起来都一样)。请求签名正确后,才会得到具体的错误代码。
| HTTP | code | 含义 |
|---|
| 401 | unauthorized | 密钥未知或签名错误。请检查待签名字符串。 |
| 401 | key_revoked, key_expired | 密钥已不能使用。 |
| 401 | stale_timestamp | 您的时钟与 FortuneX 相差超过 5 分钟。 |
| 401 | replay | 该 nonce 已被使用。 |
| 403 | ip_not_allowed | 请求来自密钥白名单之外的地址。 |
| 403 | operator_suspended | 您的账户已被暂停:不能下新注。读取和取消仍可使用。 |
| 403 | bets_disabled | FortuneX 已关闭真实 4D 投注。 |
| 403 | sandbox_key / sandbox_only | 在正式接口使用了沙盒密钥,或在沙盒接口使用了正式密钥。 |
| 404 | not_found | 路径不存在(或 API 尚未开启)。 |
| 404 | unknown_bet, unknown_statement | 在您的账户下 找不到。 |
| 400 | bad_json, bad_ref, bad_sub_account, bad_ticket, bad_date, bad_status, bad_cursor, bad_outcome, bad_win | 请求格式有误。bad_ticket 的原因写在 message 中(与 FortuneX 玩家看到的文字相同)。 |
| 400 | stake_too_small, stake_too_large | 投注单总额超出 GET /4d/games 中的限额。 |
| 402 | insufficient_prepaid | 预付运营商:余额不足以支付本次投注额。 |
| 402 | credit_limit_exceeded | 信用账期运营商:未结算信用加上本次投注额将超过您的额度。 |
| 402 | credit_not_configured | 信用账期运营商:4D 尚未设置信用额度。请联系 FortuneX。 |
| 409 | idempotency_mismatch | 该 ref 已用于另一笔不同的投注。 |
| 409 | game_unavailable | 您选择的某个彩池已关闭或未开放真实投注。 |
| 409 | capacity | FortuneX 的未结投注总额已达上限。请稍后再试。 |
| 409 | cannot_cancel, cancel_pending | 见第 7 节。 |
| 409 | not_placed, already_settled | 仅限沙盒结果调用。 |
| 409 | use_4d_api | 您对 4D 游戏使用了通用投注接口。 |
| 405 | method_not_allowed | 只使用 GET 和 POST。 |
| 429 | rate_limited, operator_limit | 请求过快。见第 12 节。 |
| 503 | maintenance, busy | 请稍后重试。 |
| 500 | internal_error | 我方问题。请使用相同的 ref 重试(第 5 节)。 |
5. 幂等性与重试
ref 是您为每笔投注设定的幂等键:1 到 100 个字符,取自 A-Z a-z 0-9 : _ . -。它在每个运营商内唯一(正式和沙盒各自独立)。您也可以改用 Idempotency-Key 请求头传同样的值;如果两者同时存在,必须相同。
- 用相同的
ref 和相同的投注单再次发送:返回 200 和同一笔投注,不会重复扣款。首次下注返回 201。 - 用相同的
ref 但投注单不同(开奖日期、彩池、号码行或 subAccount):返回 **409 idempotency_mismatch**。 - 如果下注请求超时或返回 5xx,请 **使用相同的
ref 重试**。切勿为同一个玩家操作另造新的 ref,否则会产生第二笔投注。 - 每次重试都需要新的 nonce 和时间戳(这是一个新的签名请求)。
- 除 429 外的 4xx 响应重试也不会改变结果;请修正请求。
6. 下注
请先通过 GET /4d/games 查看当前开放的彩池、接下来的开奖日期及截止时间,以及限额。
POST /4d/bets
{ "ref": "order-1001", "subAccount": "site-A-user-77", "drawDate": "2026-11-04", "pools": ["magnum", "damacai"], "lines": ["1234#1", "*2345#1"] }
subAccount:必填。您自己对该投注背后的玩家或网站的标识(1 到 100 个字符)。它与投注一起保存,并在每个视图中返回,以便 FortuneX 按子账户把您的销售与供应商的记录核对。drawDate:YYYY-MM-DD。某一期的投注在 开奖当日马来西亚时间 18:59 截止。超过 30 天以后的日期会被拒绝。pools:一个或多个:magnum、damacai、toto、singapore、sabah、sarawak、gd(Grand Dragon,豪龙)、nine(9Lotto)。sandakan 目前未在供应商处启用。当前开放的彩池请以 GET /4d/games 为准。lines:1 到 50 行,文字格式与 FortuneX 自己的投注单相同。投注单成本 = 每行投注额 × 该行展开的号码数 × 彩池数量。示例:
| 号码行 | 含义 | 每个彩池成本 |
|---|
1234#1 | 号码 1234,大(Big),RM 1 | RM 1.00 |
1234#1#1 | 大 RM 1 和小(Small)RM 1 | RM 2.00 |
1234#1#1#1 | 大、小和 A,各 RM 1 | RM 3.00 |
123#1#1 | 3D:C RM 1 和 A RM 1 | RM 2.00 |
1234**#1#1#1#1#1 | A1 至 A5 每个奖位 RM 1 | RM 5.00 |
*2345#1 | 全排列(box):全部 24 种排列,每种大 RM 1 | RM 24.00 |
800020#1 | 6D,RM 1 | RM 1.00 |
单个号码的最小投注额为 RM 0.10(1234#0.1)。最小和最大投注单金额见 GET /4d/games(limits)。5D 和 6D 投注单遵循 FortuneX 自己的开关,开关关闭时会被拒绝。开关打开时,5D 号码行只能用于彩池 toto,6D 号码行只能用于 toto、gd 或 nine;如果把这类号码行与其他彩池放在同一张投注单里,会被拒绝并返回 400 bad_ticket(提示 "Untick the other pools")。随机("R")号码行必须由您在下注前自行生成。
成本由服务器计算。您不能自行传入价格。
响应 201:
{ "ok": true, "created": true, "bet": { "ref": "order-1001", "subAccount": "site-A-user-77", "sandbox": false, "funding": "prepaid", "drawDate": "2026-11-04",
"pools": ["magnum"], "lines": ["1234#1"], "stakeSen": 100, "refundedSen": 0, "status": "pending", "statusText": "Waiting for approval", "reason": null,
"result": { "settled": false, "winSen": null, "wins": null, "settledAt": null }, "canCancel": true, "cancelState": null, "cancelNote": null, "createdAt": 1793700000123, "orders": [] } }
投注生命周期
pending ──approved by FortuneX (or automatically)──▶ sending ──supplier answers──▶ accepted | partial | refused | check
│ │
├─ rejected (a reason is given) ─▶ rejected (stake returned) ├─ cancelled (you cancelled: stake returned)
└─ you cancel ─▶ cancelled (stake returned) └─ settled: the result is recorded (win or loss)
status | 含义 | 投注额 |
|---|
pending | 等待 FortuneX 发送给供应商。 | 冻结 |
sending | 正在发送。 | 冻结 |
accepted | 供应商已接受。orders[] 显示供应商参考号。 | 冻结 |
partial | 部分号码已售罄;被拒的部分已退回(refundedSen)。 | 部分退回 |
refused | 供应商未接受。 | 退回 |
rejected | FortuneX 未批准(会显示 reason),或在批准前已截止投注。 | 退回 |
cancelled | 已取消。 | 退回 |
check | 某个订单结果未知,FortuneX 工作人员正在与供应商核实。 | 冻结,直到处理完毕 |
投注是等待人工批准还是立即发送,由 FortuneX 的设置决定;请轮询 GET /4d/bets/{ref},直到 status 不再是 pending 或 sending。
orders[] 中每个彩池一条:providerRef(FortuneX 就该订单与供应商使用的参考号)、betNo 和 receiptNo(供应商的编号,接受后才有),以及已接受/被拒的金额。与 FortuneX 或供应商对账时请使用这些信息。
查询投注
GET /4d/bets/{ref}:查询一笔投注。GET /4d/bets?drawDate=&status=&subAccount=&limit=&before=:您的投注,最新的在前。limit 最大 200(默认 50)。如果 next 不为 null,把它作为 before 传入即可获取下一页。
7. 取消
POST /4d/bets/{ref}/cancel(请求体为空或 {})。规则与 FortuneX 自己的玩家相同:
- 仍为
pending 的投注:立即取消,全额退回投注额。 - 已下单的投注(
accepted 或 partial),在 开奖即将截止之前(开奖日 18:59 前 2 分钟之内):属于 取消申请。响应显示 cancelState: "requested"。FortuneX 批准(或自动批准)后,供应商逐个订单取消,此时状态才变为 cancelled,投注额才退回。如果供应商拒绝,cancelState 变为 failed 或 declined,并带有 cancelNote,投注保持有效。请轮询该投注。 - 开奖即将截止之后,或结果已记录之后:
409 cannot_cancel。 - 重复取消:
409 cannot_cancel(申请等待期间为 cancel_pending)。
8. 预付账户与信用账期账户
FortuneX 会针对您以及每一类游戏(4D、体育、老虎机、真人娱乐场)分别设置为 预付 或 信用账期。目前通过本 API 只能使用 4D。GET /balance 和 GET /4d/games(funding)会显示适用于您的方式。
预付
- 您的预付余额由 FortuneX 保管。您付款后,工作人员为您充值(并记录银行参考号)。
- 下注时投注额 立即扣除。如果余额不足,投注被拒绝并返回
402 insufficient_prepaid,不会留下任何记录。并发下注也不会使余额低于零。 - 退回的投注额(取消、被拒、售罄部分)只会准确退回余额一次。
- 中奖 不会 自动派入您的余额。FortuneX 每周以人工方式,参考已记录的销售和结果,把您的利润记入账户。
信用账期
- 下注时不扣款。该销售会记入您的 信用占用:即销售额减去退款,且尚未被已付款对账单结清的部分。
- 每一类游戏有各自的 信用额度。如果您的占用加上本次投注额会超过额度,投注会被拒绝并返回
402 credit_limit_exceeded。信用账期游戏 没有额度(或为 0) 时,所有投注都会被拒绝(credit_not_configured)。 - 每周一,FortuneX 为已结束的一周(周一 00:00 至下周一 00:00,马来西亚时间)出具 每周对账单:总销售、退款、记录的中奖结果、净额以及谁欠谁,付款期限为周期结束后 4 天(天数可调整)。对账单一经出具即为最终版。资金到账后,由 FortuneX 工作人员记录付款,您的信用额度随之恢复。
GET /statements 列出您的对账单;GET /statements/{number} 查看一份。均为只读。
GET /balance:
{ "ok": true, "balance": { "sandbox": false, "currency": "MYR", "prepaidBalanceSen": 250000,
"games": [ { "category": "4d", "mode": "credit", "creditLimitSen": 1000000, "exposureSen": 420000, "availableSen": 580000 },
{ "category": "sports", "mode": "prepaid", "creditLimitSen": null, "exposureSen": null, "availableSen": null } ], "asOf": 1793700000123 } }
9. 结果
结果在开奖后根据供应商的官方结果自动记录,规则与 FortuneX 自己的玩家相同(使用 FortuneX 公布的赔率表;6D 若一个号码符合多个奖项,只按 最高奖项 赔付)。部分情况需要人工处理(供应商不公布的 6D 头奖号码、非常大额的中奖、需要核对的订单);此时该投注保持未结算,直到录入结果。
GET /4d/bets/{ref}/result:state 为 pending(尚未送达供应商,或正在核对)、open(已下单,未截止开奖)、awaiting_result(已截止,结果尚未记录)、settled(见 winSen 和 wins[],含奖项与号码)或 void(已退款或从未下单)。GET /4d/results/{YYYY-MM-DD}:该期各彩池的官方号码(未公布则为 null)、已知的 6D 头奖号码、您 在该开奖日的投注(含投注额和 winSen)以及合计。
中奖不会产生任何资金转移。 它只是一条记录。预付账户每周由人工入账;信用账期账户则在每周对账单上结算净额。winSen 是按 FortuneX 赔率表计算出的该笔投注的中奖金额。
10. 对账
每笔投注都带有您的 ref、您的 subAccount、FortuneX 为每个订单生成的 providerRef,以及供应商的 betNo/receiptNo。查看某一天或某一期,请使用 GET /4d/bets?drawDate= 或 GET /4d/results/{date}。对账单 CSV(由 FortuneX 工作人员提供)列出每一笔销售、退款和结果,含时间、ref 和子账户。
11. 沙盒
沙盒密钥的行为与正式密钥相同,但投注是 模拟的:
- 签名方式相同,
/4d/*、GET /balance 和 GET /health 等接口相同,并带有 "sandbox": true 标记。其他所有正式接口返回 403 sandbox_key。 - 沙盒投注使用与正式投注相同的投注单规则、限额和开放彩池进行检查,然后 立即接受。它从不发送给供应商,不影响任何余额、信用额度或对账单。
- 沙盒的 ref 与正式的 ref 相互独立。
- 模拟供应商拒绝:使用以
sbx-refuse 开头的 subAccount。投注变为 refused,投注额"退回"(refundedSen)。 - 模拟预付余额不足:使用以
sbx-nofunds 开头的 subAccount,返回 402 insufficient_prepaid。 - 模拟结果:
POST /sandbox/4d/bets/{ref}/result,请求体 { "outcome": "win", "winSen": 350000 }(winSen 默认为投注额的十倍)或 { "outcome": "loss" }。仅适用于状态为 accepted 的沙盒投注,且只能一次;之后 GET .../result 显示 state: "settled" 和 simulated: true。 - 沙盒取消是即时的。
- 正式密钥不能调用
/sandbox/*(403 sandbox_only)。
12. 限制
- 每个 IP 地址:每分钟 600 个请求(响应
429 rate_limited,并带有 Retry-After 响应头)。 - 每个密钥:按您的密钥所设限制(默认每分钟 600 个请求)。
- 每个运营商:每分钟最多 600 笔新投注(
429 operator_limit)。正式和沙盒投注分开计算:沙盒投注不会占用正式限额。 - 请求体最大 64 KB。
- 工作人员可限制所有投注的全局未结投注总额(
409 capacity)。
遇到 429 时,等待 Retry-After 指定的秒数,再用新的 nonce 重试同一请求。
13. 安全清单
- 把密钥保存在密钥管理系统中,切勿放在浏览器或手机端代码里。只从您的服务器端签名请求。
- 只从白名单中的 IP 地址调用;地址变更前请先通知 FortuneX。
- 只使用 HTTPS,并验证服务器证书。
- nonce 要不可预测且绝不重复使用;保持您的系统时钟准确(NTP)。
ref 要防止重复:由您自己的订单号生成,这样重试时会复用同一个 ref。- 记录请求 ID 和
ref,切勿记录密钥或签名。
14. 本 API 目前不提供的功能
Webhook(请改用轮询)、体育、老虎机和真人娱乐场投注,以及任何形式的资金转移:付款、充值和利润入账均由 FortuneX 工作人员在 API 之外记录。
接口参考
下面的接口参考由 OpenAPI 文件生成,内容为英文。
Account
GET /api/b2b/v1/health
Signed ping
响应
200 OK
响应结构(示例值)
{
"ok": true,
"time": 1793700000123,
"operatorId": 1,
"keyId": "fxk_example",
"sandbox": true
}
错误 Failure. error.code is stable; message is for people. Common codes: unauthorized, stale_timestamp, replay, ip_not_allowed (401/403); bad_ticket, bad_ref, bad_sub_account, bad_date, stake_too_small, stake_too_large (400);
{
"ok": false,
"error": {
"code": "insufficient_prepaid",
"message": "The balance does not cover this stake."
}
}
GET /api/b2b/v1/balance
Your prepaid balance and credit position
响应
200 OK
响应结构(示例值)
{
"ok": true,
"balance": {
"sandbox": true,
"currency": "MYR",
"prepaidBalanceSen": 1793700000123,
"games": [
{
"category": "4d",
"mode": "prepaid",
"creditLimitSen": 1793700000123,
"exposureSen": 1793700000123,
"availableSen": 1793700000123
}
],
"asOf": 1793700000123,
"note": "string"
}
}
错误 Failure. error.code is stable; message is for people. Common codes: unauthorized, stale_timestamp, replay, ip_not_allowed (401/403); bad_ticket, bad_ref, bad_sub_account, bad_date, stake_too_small, stake_too_large (400);
{
"ok": false,
"error": {
"code": "insufficient_prepaid",
"message": "The balance does not cover this stake."
}
}
4D bets
GET /api/b2b/v1/4d/games
Pools, draw dates, minimum stakes and limits
响应
200 OK
{
"ok": true,
"sandbox": true,
"enabled": true,
"funding": "prepaid",
"currency": "string",
"games": [
{
"id": "magnum",
"name": "string",
"open": true,
"reason": "string",
"supports5d6d": true
}
],
"drawDates": [
{
"drawDate": "2026-11-04",
"day": "string",
"closesAt": "2026-11-04T10:59:00Z",
"pools": [
"string"
],
"special": true
}
],
"limits": {
"minTicketSen": 100,
"maxTicketSen": 1793700000123,
"minStakePerNumberSen": 100,
"maxLines": 1,
"closesAt": "string",
"cancelUntil": "string"
}
}
错误 Failure. error.code is stable; message is for people. Common codes: unauthorized, stale_timestamp, replay, ip_not_allowed (401/403); bad_ticket, bad_ref, bad_sub_account, bad_date, stake_too_small, stake_too_large (400);
{
"ok": false,
"error": {
"code": "insufficient_prepaid",
"message": "The balance does not cover this stake."
}
}
GET /api/b2b/v1/4d/bets
List your bets, newest first
参数
| 名称 | 位置 | 必填 | 类型 | 说明 |
|---|
drawDate | 查询 | 否 | string (date) | |
status | 查询 | 否 | pending | sending | accepted | partial | refused | rejected | cancelled | check | |
subAccount | 查询 | 否 | string | max length 100 |
limit | 查询 | 否 | integer | min 1, max 200, default 50 |
before | 查询 | 否 | integer | Pass next from the previous page. |
响应
200 OK
响应结构(示例值)
{
"ok": true,
"bets": [
{
"ref": "string",
"subAccount": "string",
"sandbox": true,
"funding": "prepaid",
"drawDate": "2026-11-04",
"pools": [
"string"
],
"lines": [
"string"
],
"stakeSen": 100,
"refundedSen": 100,
"status": "pending",
"statusText": "string",
"reason": "string",
"result": {
"settled": true,
"winSen": 1793700000123,
"wins": [
{}
],
"settledAt": 1793700000123
},
"canCancel": true,
"cancelState": "requested",
"cancelNote": "string",
"createdAt": 1793700000123,
"orders": [
{
"pool": "string",
"status": "sending",
"stakeSen": 100,
"acceptedSen": 1793700000123,
"rejectedSen": 1793700000123,
"providerRef": "string",
"betNo": 1,
"receiptNo": 1
}
]
}
],
"next": 1
}
错误 Failure. error.code is stable; message is for people. Common codes: unauthorized, stale_timestamp, replay, ip_not_allowed (401/403); bad_ticket, bad_ref, bad_sub_account, bad_date, stake_too_small, stake_too_large (400);
{
"ok": false,
"error": {
"code": "insufficient_prepaid",
"message": "The balance does not cover this stake."
}
}
POST /api/b2b/v1/4d/bets
Place a 4D bet
Prepaid: the stake is deducted at once (402 insufficient_prepaid if short). Credit term: the sale is recorded against your credit (402 credit_limit_exceeded / credit_not_configured). Sandbox key: the bet is simulated (see the Sandbox tag).
参数
| 名称 | 位置 | 必填 | 类型 | 说明 |
|---|
Idempotency-Key | 请求头 | 否 | string | max length 100 |
{
"ref": "order-1001",
"subAccount": "site-A-user-77",
"drawDate": "2026-11-04",
"pools": [
"magnum"
],
"lines": [
"1234#1"
]
}
响应
200 The same ref was already placed with this ticket (nothing charged again)
{
"ok": true,
"created": true,
"bet": {
"ref": "string",
"subAccount": "string",
"sandbox": true,
"funding": "prepaid",
"drawDate": "2026-11-04",
"pools": [
"string"
],
"lines": [
"string"
],
"stakeSen": 100,
"refundedSen": 100,
"status": "pending",
"statusText": "string",
"reason": "string",
"result": {
"settled": true,
"winSen": 1793700000123,
"wins": [
{}
],
"settledAt": 1793700000123
},
"canCancel": true,
"cancelState": "requested",
"cancelNote": "string",
"createdAt": 1793700000123,
"orders": [
{
"pool": "string",
"status": "sending",
"stakeSen": 100,
"acceptedSen": 1793700000123,
"rejectedSen": 1793700000123,
"providerRef": "string",
"betNo": 1,
"receiptNo": 1
}
]
}
}
201 Placed
{
"ok": true,
"created": true,
"bet": {
"ref": "string",
"subAccount": "string",
"sandbox": true,
"funding": "prepaid",
"drawDate": "2026-11-04",
"pools": [
"string"
],
"lines": [
"string"
],
"stakeSen": 100,
"refundedSen": 100,
"status": "pending",
"statusText": "string",
"reason": "string",
"result": {
"settled": true,
"winSen": 1793700000123,
"wins": [
{}
],
"settledAt": 1793700000123
},
"canCancel": true,
"cancelState": "requested",
"cancelNote": "string",
"createdAt": 1793700000123,
"orders": [
{
"pool": "string",
"status": "sending",
"stakeSen": 100,
"acceptedSen": 1793700000123,
"rejectedSen": 1793700000123,
"providerRef": "string",
"betNo": 1,
"receiptNo": 1
}
]
}
}
错误 Failure. error.code is stable; message is for people. Common codes: unauthorized, stale_timestamp, replay, ip_not_allowed (401/403); bad_ticket, bad_ref, bad_sub_account, bad_date, stake_too_small, stake_too_large (400);
{
"ok": false,
"error": {
"code": "insufficient_prepaid",
"message": "The balance does not cover this stake."
}
}
GET /api/b2b/v1/4d/bets/{ref}
One bet with its orders
参数
| 名称 | 位置 | 必填 | 类型 | 说明 |
|---|
ref | 路径 | 是 | string | Your bet reference. pattern ^[A-Za-z0-9:_.-]{1,100}$ |
响应
200 OK
{
"ok": true,
"created": true,
"bet": {
"ref": "string",
"subAccount": "string",
"sandbox": true,
"funding": "prepaid",
"drawDate": "2026-11-04",
"pools": [
"string"
],
"lines": [
"string"
],
"stakeSen": 100,
"refundedSen": 100,
"status": "pending",
"statusText": "string",
"reason": "string",
"result": {
"settled": true,
"winSen": 1793700000123,
"wins": [
{}
],
"settledAt": 1793700000123
},
"canCancel": true,
"cancelState": "requested",
"cancelNote": "string",
"createdAt": 1793700000123,
"orders": [
{
"pool": "string",
"status": "sending",
"stakeSen": 100,
"acceptedSen": 1793700000123,
"rejectedSen": 1793700000123,
"providerRef": "string",
"betNo": 1,
"receiptNo": 1
}
]
}
}
错误 Failure. error.code is stable; message is for people. Common codes: unauthorized, stale_timestamp, replay, ip_not_allowed (401/403); bad_ticket, bad_ref, bad_sub_account, bad_date, stake_too_small, stake_too_large (400);
{
"ok": false,
"error": {
"code": "insufficient_prepaid",
"message": "The balance does not cover this stake."
}
}
POST /api/b2b/v1/4d/bets/{ref}/cancel
Cancel a bet
参数
| 名称 | 位置 | 必填 | 类型 | 说明 |
|---|
ref | 路径 | 是 | string | Your bet reference. pattern ^[A-Za-z0-9:_.-]{1,100}$ |
请求体 (可选)
响应
200 The bet after the cancel (or the request)
{
"ok": true,
"created": true,
"bet": {
"ref": "string",
"subAccount": "string",
"sandbox": true,
"funding": "prepaid",
"drawDate": "2026-11-04",
"pools": [
"string"
],
"lines": [
"string"
],
"stakeSen": 100,
"refundedSen": 100,
"status": "pending",
"statusText": "string",
"reason": "string",
"result": {
"settled": true,
"winSen": 1793700000123,
"wins": [
{}
],
"settledAt": 1793700000123
},
"canCancel": true,
"cancelState": "requested",
"cancelNote": "string",
"createdAt": 1793700000123,
"orders": [
{
"pool": "string",
"status": "sending",
"stakeSen": 100,
"acceptedSen": 1793700000123,
"rejectedSen": 1793700000123,
"providerRef": "string",
"betNo": 1,
"receiptNo": 1
}
]
}
}
错误 Failure. error.code is stable; message is for people. Common codes: unauthorized, stale_timestamp, replay, ip_not_allowed (401/403); bad_ticket, bad_ref, bad_sub_account, bad_date, stake_too_small, stake_too_large (400);
{
"ok": false,
"error": {
"code": "insufficient_prepaid",
"message": "The balance does not cover this stake."
}
}
Results
GET /api/b2b/v1/4d/bets/{ref}/result
The result of one bet
参数
| 名称 | 位置 | 必填 | 类型 | 说明 |
|---|
ref | 路径 | 是 | string | Your bet reference. pattern ^[A-Za-z0-9:_.-]{1,100}$ |
响应
200 OK
响应结构(示例值)
{
"ok": true,
"result": {
"ref": "string",
"drawDate": "2026-11-04",
"state": "pending",
"closed": true,
"status": "pending",
"stakeSen": 100,
"refundedSen": 100,
"settled": true,
"winSen": 1793700000123,
"wins": [
{}
],
"settledAt": 1793700000123,
"simulated": true
}
}
错误 Failure. error.code is stable; message is for people. Common codes: unauthorized, stale_timestamp, replay, ip_not_allowed (401/403); bad_ticket, bad_ref, bad_sub_account, bad_date, stake_too_small, stake_too_large (400);
{
"ok": false,
"error": {
"code": "insufficient_prepaid",
"message": "The balance does not cover this stake."
}
}
GET /api/b2b/v1/4d/results/{drawDate}
The official numbers of a draw and your bets of that date
参数
| 名称 | 位置 | 必填 | 类型 | 说明 |
|---|
drawDate | 路径 | 是 | string (date) | |
响应
200 OK
{
"ok": true,
"drawDate": "2026-11-04",
"closed": true,
"pools": {
"example": {
"name": "string",
"first": "string",
"second": "string",
"third": "string",
"special": [
"string"
],
"consolation": [
"string"
],
"source": "string"
}
},
"sixFirst": {
"example": "string"
},
"bets": [
{
"ref": "string",
"subAccount": "string",
"status": "pending",
"stakeSen": 100,
"refundedSen": 100,
"settled": true,
"winSen": 1793700000123
}
],
"totals": {
"bets": 1,
"stakeSen": 100,
"refundedSen": 100,
"settled": 1,
"awaiting": 1,
"winSen": 100
}
}
错误 Failure. error.code is stable; message is for people. Common codes: unauthorized, stale_timestamp, replay, ip_not_allowed (401/403); bad_ticket, bad_ref, bad_sub_account, bad_date, stake_too_small, stake_too_large (400);
{
"ok": false,
"error": {
"code": "insufficient_prepaid",
"message": "The balance does not cover this stake."
}
}
Statements
GET /api/b2b/v1/statements
Your weekly credit statements (production keys, read only)
响应
200 OK
响应结构(示例值)
{
"ok": true,
"statements": [
{
"number": "FXS-acme-20261026",
"periodStart": 1793700000123,
"periodEnd": 1793700000123,
"periodLabel": "2026-10-26 to 2026-11-01",
"salesSen": 100,
"refundsSen": 100,
"winsSen": 100,
"netSen": 1793700000123,
"saleCount": 1,
"refundCount": 1,
"resultCount": 1,
"winCount": 1,
"byCategory": {
"example": {
"salesSen": 100,
"refundsSen": 100,
"winsSen": 100,
"netSen": 1793700000123
}
},
"owes": "operator",
"amountSen": 100,
"dueAt": 1793700000123,
"status": "issued",
"overdue": true,
"payment": {
"direction": "received",
"amountSen": 100,
"reference": "string",
"recordedAt": 1793700000123
},
"issuedAt": 1793700000123
}
]
}
错误 Failure. error.code is stable; message is for people. Common codes: unauthorized, stale_timestamp, replay, ip_not_allowed (401/403); bad_ticket, bad_ref, bad_sub_account, bad_date, stake_too_small, stake_too_large (400);
{
"ok": false,
"error": {
"code": "insufficient_prepaid",
"message": "The balance does not cover this stake."
}
}
GET /api/b2b/v1/statements/{number}
One statement
参数
| 名称 | 位置 | 必填 | 类型 | 说明 |
|---|
number | 路径 | 是 | string | The statement number (for example FXS-acme-20261026) or its id. |
响应
200 OK
响应结构(示例值)
{
"ok": true,
"statement": {
"number": "FXS-acme-20261026",
"periodStart": 1793700000123,
"periodEnd": 1793700000123,
"periodLabel": "2026-10-26 to 2026-11-01",
"salesSen": 100,
"refundsSen": 100,
"winsSen": 100,
"netSen": 1793700000123,
"saleCount": 1,
"refundCount": 1,
"resultCount": 1,
"winCount": 1,
"byCategory": {
"example": {
"salesSen": 100,
"refundsSen": 100,
"winsSen": 100,
"netSen": 1793700000123
}
},
"owes": "operator",
"amountSen": 100,
"dueAt": 1793700000123,
"status": "issued",
"overdue": true,
"payment": {
"direction": "received",
"amountSen": 100,
"reference": "string",
"recordedAt": 1793700000123
},
"issuedAt": 1793700000123
}
}
错误 Failure. error.code is stable; message is for people. Common codes: unauthorized, stale_timestamp, replay, ip_not_allowed (401/403); bad_ticket, bad_ref, bad_sub_account, bad_date, stake_too_small, stake_too_large (400);
{
"ok": false,
"error": {
"code": "insufficient_prepaid",
"message": "The balance does not cover this stake."
}
}
Sandbox
POST /api/b2b/v1/sandbox/4d/bets/{ref}/result
Simulate the result of a sandbox bet (sandbox key only)
参数
| 名称 | 位置 | 必填 | 类型 | 说明 |
|---|
ref | 路径 | 是 | string | Your bet reference. pattern ^[A-Za-z0-9:_.-]{1,100}$ |
请求体
{
"outcome": "win",
"winSen": 350000
}
响应
200 The bet, now settled
{
"ok": true,
"created": true,
"bet": {
"ref": "string",
"subAccount": "string",
"sandbox": true,
"funding": "prepaid",
"drawDate": "2026-11-04",
"pools": [
"string"
],
"lines": [
"string"
],
"stakeSen": 100,
"refundedSen": 100,
"status": "pending",
"statusText": "string",
"reason": "string",
"result": {
"settled": true,
"winSen": 1793700000123,
"wins": [
{}
],
"settledAt": 1793700000123
},
"canCancel": true,
"cancelState": "requested",
"cancelNote": "string",
"createdAt": 1793700000123,
"orders": [
{
"pool": "string",
"status": "sending",
"stakeSen": 100,
"acceptedSen": 1793700000123,
"rejectedSen": 1793700000123,
"providerRef": "string",
"betNo": 1,
"receiptNo": 1
}
]
}
}
错误 Failure. error.code is stable; message is for people. Common codes: unauthorized, stale_timestamp, replay, ip_not_allowed (401/403); bad_ticket, bad_ref, bad_sub_account, bad_date, stake_too_small, stake_too_large (400);
{
"ok": false,
"error": {
"code": "insufficient_prepaid",
"message": "The balance does not cover this stake."
}
}
数据类型
Sen
Whole sen (RM 1.00 = 100).
integer (int64)
BetStatus
pending | sending | accepted | partial | refused | rejected | cancelled | check
PlaceBet
| 字段 | 类型 | 说明 |
|---|
ref 必填 | string | Your idempotency key and reference. |
subAccount 必填 | string | Your own reference for the player or site behind the bet. Stored and returned for matching. |
drawDate 必填 | string (date) | Bets close 18:59 Malaysian time on the draw day. |
pools 必填 | array of magnum | damacai | toto | singapore | sabah | sarawak | gd | nine | Pools to bet on (each pool costs the ticket total once). A 5D line is accepted only with toto; a 6D line only with toto, gd or nine (otherwise 400 bad_ticket). |
lines 必填 | array of string | Bet slip lines in the FortuneX text format. Smallest stake on one number is RM 0.10. |
Order
| 字段 | 类型 | 说明 |
|---|
pool | string | |
status | sending | accepted | refused | uncertain | cancelled | |
stakeSen | integer (sen) | Whole sen (RM 1.00 = 100). |
acceptedSen | integer (int64), may be null | Whole sen (RM 1.00 = 100). |
rejectedSen | integer (int64), may be null | Whole sen (RM 1.00 = 100). |
providerRef | string | The reference FortuneX used for this order with the supplier. |
betNo | integer, may be null | The supplier's bet number, once accepted. |
receiptNo | integer, may be null | |
Bet
| 字段 | 类型 | 说明 |
|---|
ref | string | |
subAccount | string | |
sandbox | boolean | |
funding | prepaid | credit | sandbox | |
drawDate | string (date) | |
pools | array of string | |
lines | array of string | |
stakeSen | integer (sen) | Whole sen (RM 1.00 = 100). |
refundedSen | integer (sen) | Whole sen (RM 1.00 = 100). |
status | pending | sending | accepted | partial | refused | rejected | cancelled | check | |
statusText | string | |
reason | string, may be null | |
result | object | |
canCancel | boolean | |
cancelState | requested | cancelling | declined | failed, may be null | |
cancelNote | string, may be null | |
createdAt | integer (int64) | |
orders | array of Order | |
BetEnvelope
| 字段 | 类型 | 说明 |
|---|
ok | boolean | |
created | boolean | Only on placement. |
bet | Bet Bet | |
BetResult
| 字段 | 类型 | 说明 |
|---|
ref | string | |
drawDate | string (date) | |
state | pending | open | awaiting_result | settled | void | |
closed | boolean | |
status | pending | sending | accepted | partial | refused | rejected | cancelled | check | |
stakeSen | integer (sen) | Whole sen (RM 1.00 = 100). |
refundedSen | integer (sen) | Whole sen (RM 1.00 = 100). |
settled | boolean | |
winSen | integer (int64), may be null | Whole sen (RM 1.00 = 100). |
wins | array of object, may be null | |
settledAt | integer (int64), may be null | |
simulated | boolean | True for a sandbox bet. |
Balance
| 字段 | 类型 | 说明 |
|---|
sandbox | boolean | |
currency | string | |
prepaidBalanceSen | integer (int64), may be null | Whole sen (RM 1.00 = 100). |
games | array of object | |
asOf | integer (int64) | |
note | string | |
GamesInfo
| 字段 | 类型 | 说明 |
|---|
ok | boolean | |
sandbox | boolean | |
enabled | boolean | False when real 4D bets are off or FortuneX is in maintenance. |
funding | prepaid | credit | sandbox | |
currency | string | |
games | array of object | |
drawDates | array of object | |
limits | object | |
DrawResults
| 字段 | 类型 | 说明 |
|---|
ok | boolean | |
drawDate | string (date) | |
closed | boolean | |
pools | object | Official numbers per pool id, or null when not published yet. |
sixFirst | object | The 6D first prize for gd / nine where known. |
bets | array of object | |
totals | object | |
Statement
Final once issued. net = sales - refunds - wins. Positive net: you owe FortuneX; negative: FortuneX owes you.
| 字段 | 类型 | 说明 |
|---|
number | string | |
periodStart | integer (int64) | |
periodEnd | integer (int64) | |
periodLabel | string | |
salesSen | integer (sen) | Whole sen (RM 1.00 = 100). |
refundsSen | integer (sen) | Whole sen (RM 1.00 = 100). |
winsSen | integer (sen) | Whole sen (RM 1.00 = 100). |
netSen | integer (int64) | |
saleCount | integer | |
refundCount | integer | |
resultCount | integer | |
winCount | integer | |
byCategory | object | Sales, refunds, wins and net per game category (today only 4d). |
owes | operator | fortunex | nobody | |
amountSen | integer (sen) | Whole sen (RM 1.00 = 100). |
dueAt | integer (int64) | |
status | issued | paid | |
overdue | boolean | |
payment | object, may be null | |
issuedAt | integer (int64) | |
版本
本文档描述运营商 API 的 1.0.0 版本。基础路径带有版本号(/v1);版本号来自 OpenAPI 文件,文档更新时本页会重新生成。
返回顶部