SBOBET API

API สำหรับจัดการ Member, เครดิต และรายงานของ SBOBET ผ่านบัญชี agent ทุกเส้นเป็น JSON ทั้งขาเข้าและขาออก

1แนบ API Key ทุกครั้ง

ส่ง header X-API-Key ในทุก request (หรือ Authorization: Bearer <access token>) ถ้าไม่แนบจะได้ 401 FSB-AUTH-001

2เรียก Directory ก่อน

เริ่มจาก POST /report/directory เพื่อดูรายชื่อบัญชีดาวน์ไลน์ — ได้ customerId และ accountId ไว้ใช้กับเส้นรายงานอื่นโดยไม่ต้องพิมพ์เอง

ตัวอย่างการเรียกใช้

curl -X POST {BASE_URL}/getcredit \
  -H "X-API-Key: <your-api-key>" \
  -H "Content-Type: application/json" \
  -d '{"userName": "member001"}'

เลือกเส้นที่ต้องการจากเมนูด้านซ้าย — ใช้ช่องค้นหาด้านบนเพื่อกรอง และลิงก์ของแต่ละหน้าแชร์ต่อได้ (URL มี #ชื่อเส้น ต่อท้าย)

POST Create Member

/create

Description

สร้าง Member account ใหม่

Request Body

Parameter Type Required Description
userName String Optional Username ที่ต้องการสร้าง — ไม่ส่งก็ได้: ระบบจะไล่เลขถัดไปให้อัตโนมัติ (code ของ agent เป็น prefix + เลขถัดจาก member ที่มีอยู่ เช่น zum4501000, zum4501001). ถ้าส่งมาเองต้องขึ้นต้นด้วย code ของ agent ตัวที่สร้าง ไม่งั้นพอร์ทัลปฏิเสธ (Invalid Username)
password String Optional Password (default: "123123123Zz@@")
firstName String Optional ชื่อ (default: "ABC")
lastName String Optional นามสกุล (default: "Xenter")
useUsername String Required บัญชี agent ที่ใช้สร้าง (ต้องเป็นบัญชีระดับ AG)
agentCustomerId Number Required customerId ของบัญชี agent นั้น — ระบบตรวจกับ portal ว่าตรงกับ useUsername ก่อนสร้าง ถ้าไม่ตรงจะปฏิเสธทั้งคำขอ

Request Example

// ไม่ต้องส่ง userName — ระบบไล่เลขถัดไปให้เอง (เช่น zum4501003)
{
  "password": "Test@123456",
  "firstName": "John",
  "lastName": "Doe",
  "useUsername": "zum45agent",
  "agentCustomerId": 9000001
}

// หรือกำหนดเอง (ต้องขึ้นต้นด้วย code ของ agent)
{
  "userName": "zum4501099",
  "password": "Test@123456",
  "useUsername": "zum45agent",
  "agentCustomerId": 9000001
}

Response Example

{
  "success": true,
  "message": "สร้าง Member สำเร็จ: abc88zz001",
  "userName": "abc88zz001",
  "customerId": 9100012,
  "agentCustomerId": 9000001,
  "usedUsername": "agentdemo168",
  "response": {
    "__note__": "ค่าดิบที่ portal คืนกลับมา (ผ่านตรง ๆ) — โครงสร้างขึ้นกับ portal"
  }
}

success, message, userName, customerId เป็น envelope ของบริการนี้ ส่วน response คือค่าที่ portal คืนกลับมาแบบไม่ปรับรูป (ล้มเหลวจะได้ success: false พร้อม failedAPI และ upstreamStatus)

POST Create Bulk Members

/create/bulk

Description

สร้าง Member accounts หลายๆ account พร้อมกัน (สูงสุด 100 accounts)

Request Example

{
  "userNames": ["abc88zz001", "abc88zz002", "abc88zz003"],
  "useUsername": "agentdemo168",
  "agentCustomerId": 9000001,
  "password": "123123123Zz@@",
  "firstName": "ABC",
  "lastName": "Xenter"
}

Response Example

{
  "success": true,
  "message": "สร้าง Member สำเร็จ 3/3 accounts",
  "requested": 3,
  "successCount": 3,
  "failCount": 0,
  "agentCustomerId": 9000001,
  "usedUsername": "agentdemo168",
  "createdAccounts": [
    "abc88zz001",
    "abc88zz002",
    "... เหลืออีก 1 รายการ"
  ],
  "results": [
    {
      "userName": "abc88zz001",
      "customerId": 9100012,
      "success": true
    },
    {
      "userName": "abc88zz002",
      "success": true
    },
    "... เหลืออีก 1 รายการ"
  ]
}

โครงสร้างจากผลการเรียกจริง

POST Client Login / Web Proxy

/client/login

Description

ล็อกอินผู้เล่นแล้วเปิด url ที่ตอบกลับใน browser ของลูกค้า เพื่อตั้ง cookie sbo_session และเข้าหน้าเล่น หน้าเว็บ, CSS, JavaScript และ WebSocket ใช้ proxy ที่รวมอยู่ใน full-sbobet

เรียกจาก frontend หรือ backend ของเว็บคุณได้ทุก domain โดยส่ง username/password ไม่ต้องใช้ UI ของ home และไม่ต้องส่ง API key จากนั้นเปิด url ที่ตอบกลับมาใน browser ของลูกค้าเพื่อเข้าหน้าเล่น

Request Body

{"username":"member001","password":"CurrentPassword"}

Response Example

{
  "success": true,
  "session": "<opaque-session-id>",
  "url": "https://amusementgot.com/client/entry?ticket=<one-time-ticket>",
  "play_url": "https://amusementgot.com/"
}

url ใช้ครั้งเดียวภายใน 2 นาที: GET /client/entry?ticket=... ตั้ง cookie แล้ว redirect ไป play_url URL สุดท้ายเป็น https://amusementgot.com/ รองรับกรณี browser ไม่รับ third-party cookies ให้เปิดด้วย window.location.assign(data.url) หากเรียกผ่าน backend ให้ส่ง url กลับ frontend หรือ redirect browser ไป url นั้น

GET /client/status ตรวจสถานะ cookie ปัจจุบัน; POST /client/logout ยกเลิก session ของ browser นี้ การรับทุก domain ใช้กับเส้น login ส่วน status/admin API ใช้ origin/auth เดิม

เครื่องมือจัดการ proxy อยู่ที่ /client/proxy/health, /client/proxy/metrics, /client/proxy/sessions, /client/proxy/refresh, /client/proxy/import-session, /client/proxy/cf-cookies และต้องใช้ API key หรือสิทธิ์ admin

POST Client Change Password

/client/changepassword

Description

ล็อกอินเว็บผู้เล่นด้วย username/password แล้วเรียก POST /api/Password/UpdatePassword ในคำขอเดียว ใช้ได้กับหน้ารหัสผ่านหมดอายุ ไม่ต้องมี agent JWT, PIN หรือ customerId

แนบ X-API-Key หรือ Bearer token ที่มีสิทธิ์ operator ขึ้นไป ระบบใช้ session เฉพาะคำขอนี้ และไม่เก็บรหัสผ่านหรือ session ผู้เล่น

Request Body

ParameterTypeRequiredDescription
usernameStringRequiredUsername ของผู้เล่น
passwordStringRequiredรหัสผ่านปัจจุบัน
newPasswordStringRequiredรหัสผ่านใหม่ ระบบส่ง confirmNewPassword ให้ตรงกันอัตโนมัติ
curl -X POST {BASE_URL}/client/changepassword \
  -H "X-API-Key: <your-api-key>" \
  -H "Content-Type: application/json" \
  -d '{"username":"member001","password":"OldPass123@@","newPassword":"NewPass456@@"}'

Response Example

{
  "success": true,
  "username": "member001",
  "message": "เปลี่ยนรหัสผ่านผู้เล่นสำเร็จ กรุณาล็อกอินด้วยรหัสผ่านใหม่",
  "requiresLogin": true,
  "response": {"status": "Success", "message": ""}
}

สำเร็จเมื่อ upstream ตอบ HTTP 200 และ status เป็น Success จากนั้นให้ผู้เล่นล็อกอินใหม่ด้วยรหัสใหม่ การเปลี่ยนรหัสไม่มี automatic retry

กรณีผิดพลาดมี step, upstreamStatus และ details ระบุขั้นตอนที่ล้มเหลว: 400 ข้อมูลไม่ครบหรือ upstream ปฏิเสธ, 429 ถูกจำกัดอัตรา, 503 upstream ผิดปกติหรือ timeout (กรณี timeout มี timeout: true) — ระบบนี้ไม่ตอบ 502/504 เพราะ Cloudflare จะแทน body ของสองสถานะนั้นด้วยหน้า HTML ของตัวเอง

ใช้ login rate limit และ outbound proxy ของระบบ โดเมนผู้เล่นกำหนดด้วย CLIENT_PORTAL_DOMAIN (ค่าเริ่มต้น amusementget.com) กำหนด CLIENT_OUTBOUND_PROXY เพื่อใช้ proxy แยกสำหรับ API ผู้เล่น กรณี upstream redirect ด้วย HTTP 302 ให้ตรวจว่า IP ของ proxy เข้าถึงเว็บผู้เล่นได้

POST Change Password

/changepassword

Description

เปลี่ยนรหัสผ่านของ Member

Request Body

Parameter Type Required Description
customerId Number Required Customer ID ที่ต้องการเปลี่ยนรหัสผ่าน
password String Required รหัสผ่านใหม่
accountId String Optional Account ID (ถ้าไม่ใส่จะใช้ customerId)

Request Example

{
  "customerId": 9100020,
  "accountId": "abc88zz960",
  "password": "@@123123Zz",
  "useUsername": "bot0860"
}

Response Example

{
  "success": true,
  "message": "เปลี่ยนรหัสผ่านสำเร็จสำหรับ Customer ID: 9100016",
  "customerId": 9100016,
  "accountId": "xyz69aa001",
  "usedUsername": "xyz69test",
  "profileBefore": {
    "accountId": "xyz69aa001",
    "firstName": "",
    "lastName": "",
    "phone": "",
    "mobile": "",
    "isSuspended": true,
    "isLocked": false
  }
}

โครงสร้างจากผลการเรียกจริง

POST Suspend Account

/suspend

Description

ล็อค/ปลดล็อค account

Request Body

Parameter Type Required Description
customerId Number Required Customer ID ที่ต้องการ suspend
isSuspended Boolean Required true = ล็อค, false = ปลดล็อค

Request Example

{
  "customerId": 9100017,
  "isSuspended": true,
  "useUsername": "bot0860"
}

Response Example

{
  "accountId": "xyz5911",
  "currentStatus": false,
  "customerId": 9100009,
  "message": "Customer ID 9100009 เปิดอยู่แล้ว",
  "noChangeNeeded": true,
  "success": true,
  "usedUsername": "agentdemo168"
}

POST Suspend Bulk Accounts

/suspend/bulk

Description

ล็อค/ปลดล็อคหลาย accounts พร้อมกัน (สูงสุด 50 accounts)

Request Example

{
  "customers": [9100017, 9100018, 9100019],
  "isSuspended": true,
  "useUsername": "bot0860"
}

Response Example

{
  "action": "unsuspend",
  "failCount": 0,
  "message": "เปิดบัญชี: สำเร็จ 0 accounts, ล้มเหลว 0 accounts, ไม่ต้องเปลี่ยน 2 accounts",
  "noChangeCount": 2,
  "requested": 2,
  "results": [
    {
      "accountId": "xyz5911",
      "customerId": 9100009,
      "message": "เปิดอยู่แล้ว",
      "noChangeNeeded": true,
      "statusAfter": false,
      "statusBefore": false,
      "success": true
    },
    {
      "accountId": "xyz5922",
      "customerId": 9100011,
      "message": "เปิดอยู่แล้ว",
      "noChangeNeeded": true,
      "statusAfter": false,
      "statusBefore": false,
      "success": true
    }
  ],
  "success": true,
  "successCount": 0,
  "usedUsername": "agentdemo168"
}

POST Topup Credit

/topup/credit

Description

เติมเครดิตให้กับ Member พร้อมตรวจสอบ boardtotal ก่อนเติม

Request Body

รองรับหลายรูปแบบ:

  • เติมให้ 1 คน
  • เติมให้หลายคนจำนวนเท่ากัน
  • เติมให้หลายคนจำนวนต่างกัน
Parameter Type Required Description
agentCustomerId Number Optional Agent Customer ID สำหรับดึงข้อมูลเครดิตก่อน/หลังเติม
accountId String Optional accountId ของ member (เช่น zum4501000) — ถ้าส่งมา ระบบอ่านเครดิตปัจจุบันและยอดหลังทำรายการรายคนด้วย GET /CustomerBalanceInfoByAccountId (payload คงที่ ~0.5KB ไม่โตตามจำนวนยูสใต้ agent) และไม่ต้องส่ง agentCustomerId; ไม่ส่ง = อ่านจาก /wallet/creditbalance/children ทั้งสาย (แบบเดิม) — accountId ผิด → 404 ปฏิเสธทันที, ข้ามสาย → 403; ต้องคู่กับ customerId คนเดียวกัน เพราะการเขียนใช้ customerId
asyncVerify Boolean Optional ตอบทันทีหลังเขียนเครดิตสำเร็จ แล้วตรวจยอดหลังทำรายการเบื้องหลัง (เร็วขึ้น ~1 round-trip) — verify.status จะเป็น "pending" และถ้ายอดไม่ตรงจะเข้า /reviews ตามปกติ (default: false)
boardtotal Number Optional ตรวจสอบ boardtotal (ยอดกระดาน agent) ก่อน/หลังเติม — ส่งมาเพื่อยืนยันยอด (ถ้าไม่ตรงจะไม่เติม). ไม่ส่ง = ข้ามการตรวจยอดกระดานทั้งหมด (เติมตามปกติ, verify.status = "skipped") เหมาะกับเติม MA→AG ที่ไม่มี/อ่านยอดกระดานไม่ได้

Request Examples

แนะนำ: ส่ง accountId คู่ customerId — อ่านเครดิตรายคน เหมาะกับ agent ที่มียูสเยอะ

{
  "customerId": 37475555,
  "accountId": "zum4501000", // ส่งคู่กับ customerId → อ่านเครดิตรายคน ไม่ต้องใช้ agentCustomerId
  "amount": 500,
  "boardtotal": 97700,
  "useUsername": "zum45agent"
}

หลายคนแบบรายคน (accountId ต่อ item)

{
  "customers": [
    { "customerId": 37475555, "accountId": "zum4501000", "amount": 500 },
    { "customerId": 37475556, "accountId": "zum4501001", "amount": 200 }
  ],
  "useUsername": "zum45agent"
}

รูปแบบเดิม: เติมผ่าน customers array ด้วย agentCustomerId (อ่านจาก children ทั้งสาย)

{
  "agentCustomerId": 9000001,
  "customers": [
    // { "customerId": 9100015, "amount": 160 },
    {
      "customerId": 9100012,
      "amount": 100,
      "boardtotal": 46887
    }
  ],
  "useUsername": "abc88zzq"
}

รูปแบบที่ 1: เติมให้ 1 คน

{
  "customerId": 9100012,
  "amount": 100,
  "boardtotal": 1000,
  "accountId": "bot0860",
  "agentCustomerId": 9000001,
  "useUsername": "abc88zzq"
}

รูปแบบที่ 2: เติมให้หลายคนจำนวนเท่ากัน

{
  "customerId": "9100012,9100013,9100014",
  "amount": 100,
  "agentCustomerId": 9000001
}

รูปแบบที่ 3: เติมให้หลายคนจำนวนต่างกัน

{
  "customers": [
    {
      "customerId": 9100012,
      "amount": 130,
      "boardtotal": 1000,
      "accountId": "bot0860",
      "agentCustomerId": 9000001
    },
    {
      "customerId": 9100013,
      "amount": 105
    }
  ],
  "agentCustomerId": 9000001,
  "useUsername": "abc88zzq"
}

Response Example

{
  "success": true,
  "verify": {
    "status": "ok"
  },
  "message": "เติมเครดิต: สำเร็จ 2 accounts, ล้มเหลว 0 accounts, รวม 235 บาท",
  "usedUsername": "abc88zzq",
  "summary": {
    "totalAccounts": 2,
    "successCount": 2,
    "failCount": 0,
    "skipCount": 0,
    "creditNoChangeCount": 0,
    "requestedAmount": 235,
    "actualUsedAmount": 235
  },
  "boardtotal": {
    "boardtotal_before": 10000,
    "boardtotal_after": 9765,
    "used": 235,
    "change": "10000 → 9765 (ใช้ไป 235 บาท)"
  },
  "results": [
    {
      "customerId": 9100012,
      "amount": 130,
      "success": true,
      "creditBefore": {
        "givenCredit": 100,
        "totalBalance": 50,
        "betCredit": 150
      },
      "creditAfter": {
        "givenCredit": 230,
        "totalBalance": 50,
        "betCredit": 280
      },
      "creditChange": {
        "givenCredit": 130,
        "totalBalance": 0,
        "betCredit": 130
      }
    }
  ]
}

หมายเหตุ

  • หากเครดิตปัจจุบันเท่ากับจำนวนที่ต้องการเติมอยู่แล้ว (CreditNoChange) จะนับเป็น skip
  • ระบบจะแสดงข้อมูลเครดิตก่อนและหลังเติมเมื่อระบุ agentCustomerId
  • boardtotal คือยอดเครดิตคงเหลือของ Agent
  • การเทียบยอดหลังเติม (verify): ระบบอ่านยอดกระดานก่อนและหลังเติม แล้วเทียบว่ายอดที่ขยับเท่ากับยอดที่เติมพอดี ผลอยู่ในฟิลด์ verify.status — "ok" ยอดตรง, "unverified" อ่านยอดไม่ครบจึงเทียบไม่ได้, "mismatch" ยอดไม่ตรง
  • เมื่อ verify.status = "mismatch" ระบบตอบ success: false พร้อม expected, actual และ reviewId — รายการถูกบันทึกเป็นรายการค้างตรวจสอบ รอพนักงานตรวจที่ GET /reviews
  • หากส่ง boardtotal รายตัวแล้วยอดไม่ตรงก่อนเติม รายการนั้นจะถูกข้าม (ยังไม่มีเงินเคลื่อนไหว) และถูกบันทึกเป็นรายการค้างตรวจสอบพร้อม reviewId เช่นกัน

POST Get Credit

/getcredit

Description

ดึงข้อมูลเครดิตของลูกค้าทั้งหมดภายใต้ Agent หรือเฉพาะลูกค้าที่ระบุ

Request Body

Parameter Type Required Description
agentCustomerId Number Required Agent Customer ID
customerId Number Optional Customer ID ที่ต้องการดูเฉพาะเจาะจง
useUsername String Optional ระบุ username ที่ต้องการใช้ JWT

Request Example

{
  "agentCustomerId": 9000001,
  "customerId": 9100012,
  "useUsername": "abc88zzq"
}

Response Example

{
  "success": true,
  "agentCustomerId": 9000001,
  "customerId": 9100012,
  "usedUsername": "abc88zzq",
  "data": {
    "customerId": 9100012,
    "credit": {
      "givenCredit": 1000
    },
    "balance": {
      "totalBalance": 500
    },
    "betCredit": 1500,
    "customerInfo": {
      "accountId": "abc88zz001",
      "isSuspended": false
    }
  },
  "message": "ดึงข้อมูลเครดิตสำเร็จสำหรับ Customer ID: 9100012"
}

POST Get Credit Agent

/getcreditagent

Description

ดึงยอดเครดิตของ ตัว Agent เอง จากหน้า Dashboard ของพอร์ทัลโดยตรง — availableCredit คือเครดิตที่ Agent เหลือไว้แจกจ่ายให้สายงาน ต่างจาก /getcredit ที่ดูเครดิตของลูกค้าในสายงาน

Request Body

Parameter Type Required Description
useUsername String Optional ระบุ username ที่ต้องการใช้ JWT — ไม่ระบุจะใช้บัญชีของ session นั้น

Request Example

{
  "useUsername": "abc88zzq"
}

Response Example

{
  "success": true,
  "accountId": "abc88zzq",
  "usedUsername": "abc88zzq",
  "availableCredit": 125000,
  "message": "ดึงเครดิตเอเย่นต์สำเร็จสำหรับ abc88zzq"
}

POST Transfer (Withdraw)

/transfer

Description

ถอนเงินจาก Member โดยระบบจะ:

  • อ่านยอดกระดานและ balance ของ agent ก่อนถอนทุกครั้ง (ปิดได้ด้วย skipCashBalanceCheck สำหรับกระดานที่ไม่มี Cash Balance)
  • ตรวจสอบ bet credit ว่าเพียงพอหรือไม่ โดย betCredit = givenCredit + totalBalance (totalBalance คือยอดได้/เสียของลูกค้า: บวก = ลูกค้าได้, ลบ = ลูกค้าเสีย)
  • ตัดจาก Credit (givenCredit) ก่อน หากยังไม่พอ จึงตัดส่วนที่เหลือจาก Balance (Transfer). ถ้า totalBalance ติดลบ (ลูกค้าเสีย) จะตัดจาก givenCredit อย่างเดียว ไม่แตะ Balance
  • อ่านยอดลูกค้าซ้ำหลังถอน แล้วเทียบว่าลดลงเท่ายอดที่ถอนพอดี — ผลอยู่ในฟิลด์ verify
  • ยอดไม่ตรง (ก่อนหรือหลัง) → บันทึกเป็นรายการค้างตรวจสอบ ดูได้ที่ GET /reviews

Request Body

Parameter Type Required Description
customerId Number Required Customer ID ที่ต้องการถอนเงิน
amount Number Required จำนวนเงินที่ต้องการถอน (ส่งเป็นค่าบวก)
boardtotal Number Recommended ควรส่งมาเสมอ — ยอดกระดาน (agent board total) ที่ปลายทางคาดไว้ ส่งมาเพื่อ cross-check กับยอดจริงให้ตรงกัน ถ้าไม่ตรงจะไม่ถอน และบันทึกเป็นรายการค้างตรวจสอบ (กันการทำรายการบนยอดที่คลาดเคลื่อน). ถ้าไม่ส่ง ระบบจะข้ามการ cross-check ยอดกระดานนี้
cashbalance Number Optional ไม่จำเป็นต้องส่ง — ระบบอ่าน totalBalance (net win/loss) สดเอง และ verify ก่อน/หลังถอนให้อยู่แล้ว. ส่งมาได้เป็น guard เสริม: ถ้าไม่ตรงกับค่าจริงจะไม่ถอน และบันทึกเป็นรายการค้างตรวจสอบ.
ค่าจริงเทียบกับ totalBalance ไม่ใช่ฟิลด์ cashBalance (พอร์ทัลรายงาน cashBalance = 0 ที่ระดับ agent-aggregate ยอดจริงอยู่ที่ totalBalance).
หมายเหตุ: พอร์ทัล SPA (/wallet/transfer/partialnew) ไม่รับ field นี้ในตัว transfer — การตรวจยอดทำที่ฝั่งเรา ไม่ได้ส่งต่อไปให้ค่าย
skipCashBalanceCheck Boolean Optional สำหรับกระดานที่ไม่มี Cash Balance เท่านั้น — ปกติระบบอ่าน cash balance ของ agent ก่อนถอนทุกครั้ง ตั้งเป็น true เพื่อข้ามการอ่านนี้ (default: false)
skipBetCreditCheck Boolean Optional ข้ามการเช็ค bet credit (default: false)
accountId String Optional accountId ของ member (เช่น zum4501000) — ถ้าส่งมา ระบบอ่านเครดิตปัจจุบันและยอดหลังทำรายการรายคนด้วย GET /CustomerBalanceInfoByAccountId (payload คงที่ ~0.5KB ไม่โตตามจำนวนยูสใต้ agent) และไม่ต้องส่ง agentCustomerId; ไม่ส่ง = อ่านจาก /wallet/creditbalance/children ทั้งสาย (แบบเดิม) — accountId ผิด → 404 ปฏิเสธทันที, ข้ามสาย → 403; ต้องคู่กับ customerId คนเดียวกัน เพราะการเขียนใช้ customerId
asyncVerify Boolean Optional ตอบทันทีหลังเขียนเครดิตสำเร็จ แล้วตรวจยอดหลังทำรายการเบื้องหลัง (เร็วขึ้น ~1 round-trip) — verify.status จะเป็น "pending" และถ้ายอดไม่ตรงจะเข้า /reviews ตามปกติ (default: false)
agentCustomerId Number Recommended Agent Customer ID สำหรับเช็ค bet credit (แนะนำให้ใส่เสมอ)
useUsername String Optional ระบุ username ที่ต้องการใช้ JWT

Request Examples

แนะนำ: ส่ง accountId คู่ customerId — อ่านยอดรายคน

{
  "customerId": 37475555,
  "accountId": "zum4501000", // ส่งคู่กับ customerId → อ่านยอดรายคน ไม่ต้องใช้ agentCustomerId
  "amount": 100,
  "boardtotal": 97700, // ควรส่ง — cross-check ยอดกระดานให้ตรง
  "useUsername": "zum45agent"
}

รูปแบบเดิม: ด้วย agentCustomerId (อ่านจาก children ทั้งสาย)

{
  "agentCustomerId": 9000001,
  "customerId": 9100012,
  "amount": 1,
  "boardtotal": 46887, // ควรส่ง — cross-check ยอดกระดานให้ตรง
  "useUsername": "abc88zzq" // optional
  // cashbalance ไม่ต้องส่ง — ระบบอ่าน totalBalance สดและ verify เอง
}

Response Example

{
  "success": true,
  "verify": {
    "status": "ok"
  },
  "message": "Transfer สำเร็จสำหรับ Customer ID 9100020",
  "customerId": 9100020,
  "amount": 10,
  "usedUsername": "bot0860",
  "breakdown": {
    "totalWithdrawn": 10,
    "fromCredit": 10,
    "fromBalance": 0
  },
  "balanceBefore": {
    "credit": 15,
    "totalBalance": 100,
    "betCredit": 115,
    "boardtotal": 909015.0,
    "cashBalance": -150010.35 // net balance ของ agent (อ่านจาก totalBalance) ลบ = ฝั่ง agent ติดลบ
  },
  "balanceAfter": {
    "credit": 5,
    "totalBalance": 100,
    "betCredit": 105,
    "boardtotal": 909015.0,
    "cashBalance": -150000.35
  }
}

Error Cases

  • บัญชีถูกล็อคอยู่: ไม่สามารถทำ transfer ได้
  • boardtotal/cashbalance ไม่ตรง: จะไม่ดำเนินการ transfer และบันทึกเป็นรายการค้างตรวจสอบ (ตอบ reviewId มาด้วย)
  • อ่าน cash balance ก่อนถอนไม่สำเร็จ: จะไม่ดำเนินการ transfer — ถ้ากระดานไม่มี Cash Balance ให้ส่ง skipCashBalanceCheck: true
  • ยอดหลังถอนไม่ตรง: เงินขยับไปแล้ว ระบบตอบ HTTP 200 แต่ success: false พร้อม verify.status: "mismatch" และ reviewId — รอพนักงานตรวจสอบ อย่า retry ทันที
  • เครดิตไม่พอ: เมื่อ bet credit น้อยกว่าจำนวนที่ต้องการถอน
  • ไม่พบลูกค้า: เมื่อ customerId ไม่อยู่ภายใต้ agent ที่ระบุ

หมายเหตุสำคัญ

  • ระบบจะตัดจาก Credit ก่อนเสมอ หากไม่พอจึงจะตัดจาก Balance
  • ระบบจะ suspend account อัตโนมัติก่อนทำ transfer และ unsuspend หลังเสร็จ
  • หากไม่ระบุ agentCustomerId อาจทำให้เช็ค bet credit ไม่ถูกต้อง
  • Bet Credit = Given Credit + Total Balance
  • หากต้องการข้ามการเช็ค bet credit ให้ตั้ง skipBetCreditCheck = true

GET Review Queue

/reviews?status=pending

Description

รายการฝาก/ถอนที่การเทียบยอดก่อนหรือหลังทำรายการ ไม่ตรง — ระบบบันทึกไว้เป็นรายการค้าง รอพนักงานเข้ามาตรวจสอบและปิดรายการ ตาม ขั้นตอนการเทียบยอดกระดาน/Cash Balance

Query Parameters

Parameter Type Required Description
status String Optional pending (default) | resolved | all

Response Example

{
  "success": true,
  "count": 1,
  "reviews": [
    {
      "id": "665f1a2b3c4d5e6f7a8b9c0d",
      "at": "2026-08-13T12:00:00Z",
      "kind": "withdraw",
      "stage": "after",
      "field": "credit",
      "customerId": 9100012,
      "amount": 100,
      "expected": 450,
      "actual": 460,
      "useUsername": "abc88zzq",
      "note": "ยอดลูกค้าหลังถอนไม่ตรงกับที่คาด รอพนักงานตรวจสอบ",
      "status": "pending"
    }
  ]
}

หมายเหตุ

  • kind: deposit (เติม) | withdraw (ถอน)
  • stage: before = ไม่ตรงก่อนทำรายการ (ยังไม่มีเงินเคลื่อนไหว) | after = ไม่ตรงหลังทำรายการ (เงินขยับไปแล้ว ต้องตรวจสอบก่อนทำรายการซ้ำ)
  • field: ยอดที่ไม่ตรง — boardtotal | cashbalance | credit
  • ต้องตั้งค่า MongoDB จึงจะเก็บคิวนี้ได้

POST Resolve Review

/reviews/resolve

Description

ปิดรายการค้างตรวจสอบหลังพนักงานตรวจแล้ว รายการที่ถูกปิดแล้วปิดซ้ำไม่ได้ (ได้ 404) เพื่อไม่ให้สองคนเข้าใจว่าตัวเองเป็นคนจัดการรายการเดียวกัน ต้องใช้สิทธิ์ระดับ operator ขึ้นไป

Request Body

Parameter Type Required Description
id String Required id ของรายการค้างตรวจสอบ (จาก GET /reviews)
resolution String Optional บันทึกผลการตรวจสอบ

Request Example

{
  "id": "665f1a2b3c4d5e6f7a8b9c0d",
  "resolution": "ตรวจแล้ว ยอดตรงหลังรอบตัดยอด"
}

Response Example

{
  "success": true,
  "id": "665f1a2b3c4d5e6f7a8b9c0d"
}

GET Agent List

/agentlist

Description

รายชื่อ ทุกบัญชีในระบบของเรา แบบแบน (หนึ่งแถวต่อหนึ่ง บัญชี) — ไม่กรองสถานะ ไม่ว่าจะเปิด/ปิด (disabled), มี token หรือหมดอายุ ก็แสดงครบทุกตัว พร้อมระดับ (level/levelName) และสถานะ session (hasToken, expired). บัญชีที่อยู่ใน agent pool จะแนบข้อมูล pool เพิ่ม (inPool, agentCustomerId, useUsername, memberCount, active) — เลข member มาจากการ scan/นับล่าสุด ไม่ยิงพอร์ทัลสด ถ้าต้องการเลขล่าสุดให้เรียก POST /pool/{prefix}/refresh-count ก่อน

Query Parameters

ไม่มี — ส่งเฉพาะ header Authorization: Bearer <token>. member count ของ agent ใน pool ถูกนับสดจากพอร์ทัลทุกครั้งที่เรียก (ยิงพอร์ทัล 1 ครั้งต่อ agent — อย่าเรียกถี่เกินไป).

Response Example

{
  "success": true,
  "total": 333,
  "agents": [
    {
      "accountId": "bot0860",
      "accountCode": "zds0860",
      "customerId": 3021111,
      "parentCode": "zds08",
      "parentAccountId": "onlinezds08",
      "parentCustomerId": 19700001,
      "parentName": "ZDS08 (MA)",
      "level": 2,
      "levelName": "AG",
      "disabled": true,
      "hasToken": true,
      "expired": true,
      "inPool": false,
      "memberCount": 0,
      "usable": false,
      "active": false
    },
    {
      "accountId": "zum4501",
      "accountCode": "zum4501",
      "level": 2,
      "levelName": "AG",
      "disabled": false,
      "hasToken": true,
      "expired": false,
      "inPool": true,
      "prefix": "zum45",
      "agentCustomerId": 12345678,
      "useUsername": "zum45agent",
      "memberCount": 312,
      "state": "active",
      "usable": true,
      "active": true,
      "countedAt": "2026-08-17T14:15:58Z"
    }
  ]
}

หมายเหตุ

  • accountId: username ที่เก็บในระบบเรา (key ของ store)
  • accountCode: agent code ฝั่งพอร์ทัล (จาก claim ของ token เช่น zds0860)
  • customerId: customerId ของบัญชีตัวเองฝั่งพอร์ทัล (ทุกระดับ รวม MA/SMA)
  • parentCode / parentAccountId / parentCustomerId / parentName: MA/สายบนที่ AG นี้อยู่ใต้ — code, login (accountId), customerId ฝั่งพอร์ทัล และชื่อของ MA. parentAccountId ใช้เป็น useUsername ได้เลย เวลาจะเติมเครดิต/โอนให้ agent ตัวนี้ (ต้องยิงด้วย session ของตัวแม่). มาจาก org chart ที่ crawl มา; agent ที่ pool สร้างใหม่ fallback ใช้ MA ของ pool. ว่างถ้ายังไม่มีข้อมูลสาย
  • level / levelName: ระดับบัญชี — 2=AG (Agent), 3=MA (Master Agent), 4=SMA (Super Master); 0/"" = ยังไม่ทราบ
  • disabled: ถูกปิดใช้งานในระบบเรา (ยังแสดงในลิสต์นี้เสมอ ไม่ถูกกรองออก)
  • hasToken / expired: สถานะ session — มี token ไหม และหมดอายุหรือยัง
  • inPool: บัญชีนี้อยู่ใน agent pool ไหม; ถ้า true จะมี prefix, agentCustomerId, useUsername, memberCount, state, active แนบมาด้วย
  • memberCount: จำนวนยูสเซอร์ภายใต้ agent — นับสดจากพอร์ทัลทุกครั้งที่เรียก (countedAt = เวลาที่นับ); 0 ถ้าไม่อยู่ใน pool
  • active: เป็น agent ตัวที่ pool ใช้สร้างยูสใหม่อยู่ตอนนี้
  • เส้นนี้ดึงจาก store ของเรา — แสดงทุกบัญชีเสมอ ไม่กรองตามสถานะเปิด/ปิด

POST Agent Log

/agentlog

Description

ค้นหา action log ของเอเย่นต์จากพอร์ทัล (proxy ไปที่ agentlog/filter) — ประวัติการปรับเครดิต, โอนเงิน, table limit, สถานะบัญชี, คอมมิชชั่น, ธนาคาร, ฝาก-ถอน และโปรโมชั่น ตามช่วงเวลาและผู้ทำรายการ body ส่งต่อไปพอร์ทัลตามเดิมทุก field ยกเว้น useUsername

Request Body

Parameter Type Required Description
useUsername String Required บัญชีเอเย่นต์ที่ใช้ session (เช่น abc88)
fromDate Number Required เริ่มช่วงเวลา (unix milliseconds)
toDate Number Required สิ้นสุดช่วงเวลา (unix milliseconds)
createdBy String[] Optional กรองผู้ทำรายการด้วย id ของบัญชีในพอร์ทัล เช่น ["online@abc88"] (ไม่ใช่ชื่อที่ใช้ล็อกอิน) — ไม่ส่งหรือส่ง [] = ทุกคนในบัญชี; id ที่ใช้ได้ดูจาก createdBy ในคำตอบ
actionLogTabs Number[] Optional ประเภท log ที่ต้องการ (default [1] = Credit)
targetAccountId String Optional กรองเฉพาะบัญชีปลายทาง (null = ทั้งหมด)

Request Example

{
  "useUsername": "abc88",
  "fromDate": 1786507200000,
  "toDate": 1787025600000,
  "createdBy": ["online@abc88"],
  "actionLogTabs": [1],
  "targetAccountId": null
}

Response Example

{
  "success": true,
  "createdBy": ["online@abc88", "abc88"],
  "logs": {
    "creditAgentActionLogs": [
      {
        "fromValue": "1,200,000.00",
        "toValue": "1,500,000.00",
        "createOn": 1786536220563,
        "accountId": "abc8801",
        "createdBy": "online@abc88",
        "description": "Credit",
        "remark": "Successful;;",
        "ip": "203.0.113.25"
      }
    ],
    "transferAgentActionLogs": [],
    "tableLimitAgentActionLogs": [],
    "accountStatusAgentActionLogs": [],
    "commissionAgentActionLogs": [],
    "bankAgentActionLogs": [],
    "depositWithdrawalAgentActionLogs": [],
    "promotionAgentActionLogs": []
  }
}

หมายเหตุ

  • actionLogTabs เลือกได้หลายค่าพร้อมกัน — แต่ละ tab เติมข้อมูลลง array ของประเภทนั้นใน response (tab ที่ไม่ได้เลือกจะเป็น array ว่าง)
  • createOn เป็น unix milliseconds
  • fromValue/toValue เป็นสตริงมี comma ตามที่พอร์ทัลส่งมา
  • ต้องมี session ของบัญชีใน useUsername อยู่แล้ว (login ผ่านระบบก่อน)

POST Backup Domains

/domains/backup

Description

ดึงรายชื่อโดเมนสำรอง (backup domains) ของฝั่งพอร์ทัลเอเย่นต์ (proxy ไปที่ GET domains/backup) ใช้เช็คว่ามีโดเมนไหนใช้เข้าระบบแทนได้เมื่อโดเมนหลักถูกบล็อก

Request Body

Parameter Type Required Description
useUsername String Required บัญชีเอเย่นต์ที่ใช้ session (เช่น abc88)

Request Example

{
  "useUsername": "abc88"
}

Response Example

{
  "success": true,
  "domains": [
    "example-backup1.example.com",
    "example-backup2.example.com"
  ]
}

หมายเหตุ

  • domains ส่งต่อโครงตามที่พอร์ทัลตอบมาตรง ๆ — รูปแบบอาจเปลี่ยนตามฝั่งพอร์ทัล
  • ต้องมี session ของบัญชีใน useUsername อยู่แล้ว (login ผ่านระบบก่อน)

GET Analytics

/analytics/*

Description

แปดเส้นที่หน้า Control Room ใช้ — อ่านจาก MongoDB ที่ตัวเก็บข้อมูลเก็บไว้ ไม่ยิงพอร์ทัลเลย จึงเรียกถี่ได้และไม่กินโควตา switch_account ทุกเส้นเป็น GET รับพารามิเตอร์ทาง query string ไม่มี body และเปิดให้ตั้งแต่ระดับ viewer ขึ้นไป

วันที่ทุกตัวเป็น YYYY-MM-DD ตามวันแพ้/ชนะของพอร์ทัล (แบบเดียวกับ /history/*) ไม่ใช่วันตามปฏิทินสากล ถ้ายังไม่ได้ตั้ง MONGO_URI ทุกเส้นตอบ 501 พร้อมข้อความว่าการเก็บประวัติยังไม่ได้เปิดใช้งาน

Endpoints

PathQueryตอบอะไร
/analytics/today date (ไม่ใส่ = วันนี้) วันนั้นรายชั่วโมง เทียบกับชั่วโมงเดียวกันเมื่อวาน + ผู้เล่นวันนี้ กำลังเล่นตอนนี้ บิลค้าง และบิล/นาที
/analytics/series from to (≤400 วัน, ไม่ใส่ = 30 วันล่าสุด) · by=day|hour · group=none|product turnover / win-loss / จำนวนบิล เป็นราย bucket · by=hour ใช้ได้กับวันเดียว (from เท่ากับ to) เท่านั้น
/analytics/top from to (≤92 วัน, ไม่ใส่ = 7 วันล่าสุด) · metric=turnover|winloss · kind=player|agent · limit 1–100 (ค่าเริ่ม 20) อันดับผู้เล่นหรือสาย พร้อม avgStake สินค้าหลัก IP ล่าสุด (เฉพาะ player) และ spark 14 วัน
/analytics/heatmap from to (≤92 วัน, ไม่ใส่ = 28 วันล่าสุด) จำนวนบิลแยกตามวันในสัปดาห์ × ชั่วโมง — ใช้ดูว่าคึกคักช่วงไหน
/analytics/behaviour from to (≤92 วัน, ไม่ใส่ = 7 วันล่าสุด) สัดส่วน live/pre-match, บิลชุด vs เดี่ยว, ลีกและประเภทบิลยอดนิยม, ผู้เล่นใหม่/กลับมา/หายไป
/analytics/players window=7d|30d (ค่าเริ่ม 7d) ตารางความเสี่ยงรายผู้เล่น พร้อมธง highStake sharedIP winStreak inactive
/analytics/exposure matchLimit · playerLimit — ทั้งคู่ 1–100 (ค่าเริ่ม 20) บิลที่ยังไม่เคลียร์ทั้งระบบ ณ ตอนนี้ — ยอดเสี่ยงรวม จำนวนใบ แมตช์ที่เงินค้างมากสุด และผู้เล่นที่ถือมากสุด
/analytics/coverage days 1–400 (ค่าเริ่ม 30) ความครบของข้อมูลรายวันจาก coverage_daily ที่รอบกลางคืนคำนวณไว้

Response Example

GET /analytics/today

{
  "date": "2026-08-29",
  "hourly": [
    { "hour": 13, "turnover": 1840500, "winloss": -24100, "bets": 912 }
  ],
  "yesterdayHourly": [
    { "hour": 13, "turnover": 1712300, "winloss": 18700, "bets": 874 }
  ],
  "playersToday": 1284,
  "playingNow": 63,
  "exposureStake": 4180000,
  "exposureTickets": 2140,
  "betsToday": 10422,
  "betsPerMinute": 11.6,
  "asOf": "2026-08-29T13:42:05+07:00"
}

playingNow นับ betAccountId ที่ไม่ซ้ำกันในบิลค้างซึ่งเห็นล่าสุดภายใน 15 นาที ไม่ใช่จำนวนคนที่เปิดหน้าเว็บอยู่

ข้อควรรู้

  • /analytics/coverage อ่านค่าที่คำนวณไว้อย่างเดียว วันที่รอบกลางคืนยังไม่ได้คำนวณจะตอบ coverage: null — null แปลว่ายังไม่ได้ตรวจ ไม่ใช่เก็บไม่ได้เลย ต้องแยกสองอย่างนี้ในการแสดงผล
  • ตัวเลขทั้งหมดครบเท่าที่ตัวเก็บข้อมูลเก็บมาได้ ถ้า coverage ของช่วงนั้นต่ำ ตัวเลขก็ต่ำตาม — ตรวจ /analytics/coverage ก่อนสรุปว่ายอดตก
  • ช่วงวันที่ที่กว้างเกินเพดานของแต่ละเส้นตอบ FSB-INPUT-004 (400) ไม่ใช่ตัดช่วงให้เงียบๆ
  • รอบคำนวณรายวัน (analytics_daily, coverage_daily) ทำงานหลัง 06:00 น. เวลาไทย จึงยังไม่มีตัวเลขของ "เมื่อวาน" ก่อนเวลานั้น

รหัสข้อผิดพลาด

ทุกคำตอบที่ล้มเหลวจะมี code · error · fix

หน้าตาของคำตอบเมื่อล้มเหลว

{
  "success": false,
  "code": "FSB-CREDIT-003",
  "error": "boardtotal ไม่ตรง: ระบบ 250000.50 แต่ใน agent 249500.50",
  "fix": "อ่านยอดล่าสุดด้วย /getcredit แล้วส่งมาใหม่ ..."
}
  • พอร์ทัลล้มเหลว = 503 เสมอ ไม่ใช่ 502/504 — API นี้อยู่หลัง Cloudflare ซึ่งจะแทน body ของ 502/504 ด้วยหน้า HTML ของตัวเอง ผู้เรียกจะเห็นแค่ "Bad gateway" โดยไม่มีเหตุผล จึงตอบเป็น 503 พร้อม JSON แทน (code, upstreamStatus, details)
  • retryable: true + header Retry-After มีเฉพาะเมื่อความล้มเหลวนั้นยังไม่ได้เปลี่ยนอะไรบนพอร์ทัล (งานอ่าน หรือขั้นเตรียม session ก่อนเขียน) — ลองใหม่ได้ทันทีอย่างปลอดภัย ถ้าไม่มี field นี้ อย่า retry อัตโนมัติ โดยเฉพาะคำสั่งเติม/โอน เพราะผลอาจเกิดไปแล้ว ให้ตรวจยอดก่อน
  • 503 + sessionRecovering: true — session ของบัญชีพอร์ทัลหมดอายุ และพอร์ทัลยังไม่รับการล็อกอินจากระบบ (เช่นกำลังจำกัด IP ของระบบ) ยังไม่ได้ทำรายการใด ๆ ระบบจะล็อกอินให้เองเมื่อพอร์ทัลรับ ให้ลองใหม่ตาม Retry-After — ผู้เรียกไม่ต้องล็อกอินเอง (เดิมกรณีนี้ตอบ 401 "กรุณาล็อกอินใหม่" ซึ่งผิด) ส่วน 401 refreshable: false เหลือเฉพาะกรณีที่ระบบแก้เองไม่ได้จริง: ไม่ได้เก็บรหัสไว้ หรือรหัสที่เก็บไว้ถูกพอร์ทัลปฏิเสธ
  • 429 จากพอร์ทัล (rateLimited: true) มาพร้อม header Retry-After และ retryAfterSeconds เสมอ — ให้รอตามนั้น พอร์ทัลจำกัดตาม IP ขาออกของระบบนี้ ไม่ใช่ตามบัญชี การยิงซ้ำเร็วกว่านั้นไม่ช่วยให้ผ่าน ถ้ามี heldBack: true แปลว่าคำขอนี้ ยังไม่ได้ถูกส่งไปพอร์ทัล: พอร์ทัลเพิ่งปฏิเสธ ระบบจึงพักการเรียกทั่วไปไว้ก่อน (เติม/โอนเครดิตไม่ถูกพัก)
  • error เขียนให้คนอ่าน มีรายละเอียดของคำขอนั้นๆ และอาจถูกแก้คำ
  • code ไม่เปลี่ยน ใช้แยกกรณีในโค้ดและใช้ค้นหาต้นตอ
  • fix คือสิ่งที่ควรทำต่อ

คำขอไม่ถูกต้อง

CodeHTTPหมายถึงแก้ยังไง
FSB-INPUT-001400body ไม่ใช่ JSON ที่อ่านได้ตรวจว่าส่ง Content-Type: application/json และ body เป็น JSON ที่ถูกต้อง
FSB-INPUT-002400ไม่ได้ส่งค่าที่จำเป็นดูชื่อฟิลด์ที่ขาดในข้อความ แล้วส่งมาให้ครบ
FSB-INPUT-003400ค่าที่ส่งมาผิดรูปแบบหรืออยู่นอกช่วงที่รับได้ดูชนิดและช่วงค่าที่รับได้ในเอกสารของเส้นนั้น
FSB-INPUT-004400ช่วงวันที่ผิดรูปแบบ กว้างเกินไป หรือ from อยู่หลัง toใช้รูปแบบ YYYY-MM-DD และให้ from ไม่เกิน to

การเข้าใช้ระบบนี้

CodeHTTPหมายถึงแก้ยังไง
FSB-AUTH-001401ไม่ได้แนบ API key หรือ session ที่ใช้ได้แนบ X-API-Key หรือ Authorization: Bearer <access token>
FSB-AUTH-002401ชื่อผู้ใช้หรือรหัสผ่านของระบบนี้ไม่ถูกต้องตรวจชื่อผู้ใช้และรหัสผ่าน
FSB-AUTH-003401session หมดอายุหรือถูกยกเลิกแล้วเรียก /auth/refresh ด้วย refresh token หรือเข้าสู่ระบบใหม่
FSB-AUTH-004403สิทธิ์ไม่พอสำหรับงานนี้ใช้ token ที่มีสิทธิ์ระดับที่ระบุในข้อความ หรือขอให้แอดมินเพิ่มสิทธิ์
FSB-AUTH-005401บัญชีนี้เปิดยืนยันตัวตนสองชั้นไว้ ต้องส่งรหัส 6 หลักมาด้วยส่ง code จากแอป authenticator มาในคำขอเดียวกัน
FSB-AUTH-006401รหัสยืนยันสองชั้นไม่ถูกต้องขอรหัสใหม่จากแอป และตรวจว่าเวลาบนเครื่องตรงกับเวลาจริง
FSB-AUTH-007401รหัสถูกต้องแต่หมดอายุไปแล้วใช้รหัสปัจจุบันจากแอป อย่ากรอกทิ้งไว้นานก่อนกดยืนยัน

บัญชีพอร์ทัลและ Token

CodeHTTPหมายถึงแก้ยังไง
FSB-TOKEN-001400ยังไม่มี JWT ของบัญชีพอร์ทัลที่จะใช้เรียก POST /login เพื่อล็อกอินบัญชีพอร์ทัลก่อน
FSB-TOKEN-002401JWT ของบัญชีพอร์ทัลหมดอายุและต่ออายุอัตโนมัติไม่ได้ล็อกอินบัญชีพอร์ทัลใหม่ หรือเก็บรหัสไว้เพื่อให้ระบบต่อให้เอง
FSB-TOKEN-003403บัญชีพอร์ทัลนี้ถูกปิดใช้งานไว้ในระบบเปิดใช้งานที่หน้าสถานะล็อกอิน หรือเลือกบัญชีอื่น
FSB-TOKEN-004403บัญชีที่ล็อกอินอยู่ไม่มีสิทธิ์สลับไปยังบัญชีที่ขอเลือกบัญชีจากรายการที่สลับได้ หรือให้ผู้ดูแลพอร์ทัลเปิดสิทธิ์ให้
FSB-TOKEN-005400บัญชีที่ขอสลับไปไม่ใช่บัญชีเอเย่นต์สมาชิกไม่ใช่บัญชีที่สลับเข้าไปได้ ให้ระบุด้วย customerId แทน
FSB-TOKEN-006409พอร์ทัลให้สลับบัญชีได้ครั้งเดียวต่อการล็อกอิน และครั้งนี้ใช้ไปแล้วเก็บรหัสพอร์ทัลไว้เพื่อให้ระบบล็อกอินใหม่ให้เอง หรือล็อกอินใหม่เอง

เครดิต

CodeHTTPหมายถึงแก้ยังไง
FSB-CREDIT-001503อ่านเครดิตปัจจุบันไม่สำเร็จ จึงไม่ทำรายการต่อลองใหม่อีกครั้ง — ระบบยกเลิกไว้แทนที่จะเดา เพราะเดาผิดคือเขียนทับเครดิตลูกค้า
FSB-CREDIT-002400ไม่พบลูกค้ารายนี้ในสายงานของ agent ที่ระบุตรวจ agentCustomerId ว่าเป็นต้นสายของลูกค้าคนนี้จริง
FSB-CREDIT-003409boardtotal ที่ส่งมาไม่ตรงกับยอดจริงที่พอร์ทัลอ่านยอดล่าสุดด้วย /getcredit แล้วส่งมาใหม่ หรือไม่ต้องส่ง boardtotal ถ้าไม่ต้องการให้ตรวจ
FSB-CREDIT-004409cashbalance ที่ส่งมาไม่ตรงกับยอดจริงที่พอร์ทัลอ่านยอดล่าสุดแล้วส่งมาใหม่ หรือไม่ต้องส่งถ้าไม่ต้องการให้ตรวจ
FSB-CREDIT-005400เครดิตคงเหลือไม่พอสำหรับจำนวนที่ขอดึงออกลดจำนวนลง หรือถ้าติดที่เครดิตที่ผูกกับบิลค้าง ให้ส่ง skipBetCreditCheck: true เมื่อยอมรับผลได้
FSB-CREDIT-006400amount ต้องมากกว่า 0เส้นเติมและเส้นดึงออกใช้จำนวนบวกทั้งคู่ ทิศทางกำหนดด้วยเส้นที่เรียก ไม่ใช่ด้วยเครื่องหมาย

สมาชิก

CodeHTTPหมายถึงแก้ยังไง
FSB-MEMBER-001404ไม่พบสมาชิกรายนี้ตรวจ customerId — ใช้ /getcredit หรือ /report/directory เพื่อดูรายชื่อที่มีจริง
FSB-MEMBER-002409ชื่อผู้ใช้นี้ถูกใช้ไปแล้วเปลี่ยนชื่อผู้ใช้ หรือให้ระบบสุ่มต่อท้ายให้

รายงาน

CodeHTTPหมายถึงแก้ยังไง
FSB-REPORT-001400ช่วงวันที่กว้างเกินกว่าที่พอร์ทัลตอบได้แบ่งเป็นช่วงสั้นลง หรือใช้เส้น /history/* ที่อ่านจากข้อมูลที่เก็บไว้
FSB-REPORT-002404ยังไม่เคยเก็บข้อมูลของช่วงวันที่นี้ไว้ใช้ /history/coverage ดูว่ามีวันไหนบ้าง หรือเปิดดูรายงานวันนั้นหนึ่งครั้งเพื่อให้ระบบเก็บ

Gateway

CodeHTTPหมายถึงแก้ยังไง
FSB-GW-001400path ที่ส่งมาไม่ใช่ path เปล่า — เป็น URL เต็ม ชื่อโฮสต์ หรือมี ".."ส่งเฉพาะ path เช่น "/report/winloss"
FSB-GW-002403เส้นนี้ไม่อนุญาตให้เรียกผ่าน gatewayใช้เส้นเฉพาะของระบบที่ทำงานเดียวกัน ดูชื่อเส้นในข้อความ
FSB-GW-003400method ที่ส่งมาไม่รองรับใช้ GET, POST, PUT, PATCH หรือ DELETE

พอร์ทัลปลายทาง

CodeHTTPหมายถึงแก้ยังไง
FSB-PORTAL-001503พอร์ทัลตอบกลับเป็นข้อผิดพลาดดู details และ upstreamStatus ในคำตอบ — เป็นข้อความจากพอร์ทัลโดยตรง
FSB-PORTAL-002503พอร์ทัลไม่ตอบภายในเวลาที่กำหนดลองใหม่อีกครั้ง ถ้าเกิดต่อเนื่องแปลว่าพอร์ทัลกำลังมีปัญหา
FSB-PORTAL-003429พอร์ทัลปฏิเสธเพราะเรียกถี่เกินไป และลองซ้ำให้แล้วยังไม่ผ่านลดความถี่ในการเรียกลง

การจำกัดอัตราและ IP

CodeHTTPหมายถึงแก้ยังไง
FSB-LIMIT-001429เรียกถี่เกินขีดจำกัดของระบบนี้รอสักครู่แล้วลองใหม่ ดู Retry-After ในหัวข้อคำตอบ
FSB-LIMIT-002403ที่อยู่ IP นี้ไม่อยู่ในรายการที่อนุญาตเรียกจาก IP ที่อยู่ใน allow list หรือให้แอดมินเพิ่ม IP นี้เข้าไป
FSB-LIMIT-003403ที่อยู่ IP นี้ถูกบล็อกไว้ติดต่อแอดมินเพื่อปลดบล็อก

ระบบ

CodeHTTPหมายถึงแก้ยังไง
FSB-SYS-001501ฟีเจอร์นี้ต้องใช้ MongoDB แต่ยังไม่ได้ตั้งค่าไว้ตั้งค่า MONGO_URI แล้วเริ่มเซิร์ฟเวอร์ใหม่
FSB-SYS-002500ข้อผิดพลาดภายในระบบแจ้งรหัสนี้พร้อมเวลาที่เกิดให้ผู้ดูแล — รายละเอียดอยู่ในบันทึกของเซิร์ฟเวอร์

POST Win/Loss Report

/report/winloss

Description

รายงานแพ้/ชนะ แยกตามดาวน์ไลน์และผลิตภัณฑ์ คืนยอดรวมของทุกคอลัมน์มาให้ในฟิลด์ totals

⚠️ เรื่องวันที่ — สำคัญที่สุด

  • ส่งวันที่เป็น "YYYY-MM-DD" ธรรมดา เช่น "2026-07-29" เท่านั้น
  • เซิร์ฟเวอร์แปลงเป็น epoch ให้เอง โดยใช้ เที่ยงคืน UTC-4 ซึ่งเป็นเส้นแบ่งวัน (win/lost date) ของ upstream
  • ห้ามคำนวณ epoch เอง — ถ้าใช้ UTC midnight หรือ GMT+7 ยอดจะเหลื่อมไปทั้งวัน
  • เว้น to ไว้ = รายงานวันเดียว, เว้นทั้งคู่ = วันนี้

Request Body

{
  "customerId": 9000001,
  "requesterId": 9000001,
  "from": "2026-07-01",
  "to": "2026-07-29",
  "filters": [1, 3, 4, 2, 5, 6],
  "simple": false,
  "useUsername": "optional"
}
  • requesterId — เว้นไว้ได้ ค่าเริ่มต้นเท่ากับ customerId. เวลา drill-down เข้าดาวน์ไลน์ ให้ เปลี่ยนแค่ customerId แล้วคง requesterId เป็นบัญชีที่ล็อกอิน
  • filters — 1 = กีฬา, 2 = คาสิโน, 3 = เกม (4-6 upstream รับแต่ไม่เคยคืนข้อมูล) เว้นไว้ = ครบทุกตัว
  • simple: true — ยุบชั้น Agent/MA/SMA เหลือชุดตัวเลขเดียว พร้อมฟิลด์ payout

Response Example

{
  "success": true,
  "customerId": 9000001,
  "from": "2026-07-29",
  "to": "2026-07-29",
  "mode": "detailed",
  "count": 27,
  "data": [
    {
      "displayName": "Efootball",
      "filter": 1,
      "customerId": 9100008,
      "accountId": "abc78",
      "turnover": 69540,
      "playerWinloss": -69540,
      "maTotal": 521.55,
      "smaTotal": 55214.76,
      "grossCommission": 521.55,
      "companyWinloss": 13803.69
    }
  ],
  "totals": { "turnover": 750000, "companyWinloss": 18420.55 }
}

POST Dashboard

/report/dashboard

Description

รวม 9 เส้น dashboard ของ upstream ให้เหลือ request เดียว ยิงขนานฝั่งเซิร์ฟเวอร์ — ออกแบบมาให้เรียกซ้ำเป็นรอบ (realtime)

Request Body

{
  "accountId": "abc",
  "useUsername": "optional"
}

accountId คือ username ที่ได้จาก /report/directory (เช่น abc, xyz, qrs)

Response Example

{
  "success": true,
  "fetchedAt": "2026-07-29T05:20:11Z",
  "data": {
    "totalOutstanding": 356175.68,
    "remainingCredit": 0,
    "usedCredit": 50000000,
    "players": { "online": 73, "open": 35152 },
    "monthToDate": {
      "previous": 610000.15,
      "current": 830000.60,
      "trend": { "value": 1.0648, "display": true }
    },
    "inactivePlayers": [ { "accountId": "abc8810069", "betCredit": 0.43 } ]
  },
  "errors": null
}

ถ้ามีบางเส้นล้ม จะไม่ทำให้ทั้ง request พัง — คีย์นั้นจะหายไปจาก data แล้วไปโผล่ใน errors แทน ให้ tile ที่เหลือยังแสดงผลได้ (จะได้ 503 ก็ต่อเมื่อล้มทั้งหมด ซึ่งมักแปลว่า token หมดอายุ)

POST Metrics

/report/metrics

Description

time series แพ้/ชนะรายวัน แยกตามกลุ่มผลิตภัณฑ์ ใช้วาดกราฟ

Request Body

{
  "accountId": "abc",
  "filters": [1, 2, 3]
}

คีย์ของ data เป็นชื่อกลุ่มภาษาไทย (กีฬา / คาสิโน / เกม) แต่ละค่ามี current และ lastYear เป็น array ของ { labels: [epoch, epoch], value }

Response Example

{
  "accountId": "xyz59",
  "data": {
    "กีฬา": [
      {
        "productType": 1,
        "current": [
          {
            "labels": [
              1785988800000,
              1785988800000
            ],
            "value": 0
          },
          {
            "labels": [
              1786075200000,
              1786075200000
            ],
            "value": 0
          },
          "... เหลืออีก 5 รายการ"
        ],
        "lastYear": [
          {
            "labels": [
              1754452800000,
              1754452800000
            ],
            "value": 0
          },
          {
            "labels": [
              1754539200000,
              1754539200000
            ],
            "value": 0
          },
          "... เหลืออีก 5 รายการ"
        ]
      },
      {
        "productType": 2,
        "current": [
          {
            "labels": [
              1785988800000,
              1785988800000
            ],
            "value": 0
          },
          {
            "labels": [
              1786075200000,
              1786075200000
            ],
            "value": 0
          },
          "... เหลืออีก 5 รายการ"
        ],
        "lastYear": [
          {
            "labels": [
              1754452800000,
              1754452800000
            ],
            "value": 0
          },
          {
            "labels": [
              1754539200000,
              1754539200000
            ],
            "value": 0
          },
          "... เหลืออีก 5 รายการ"
        ]
      },
      "... เหลืออีก 1 รายการ"
    ],
    "คาสิโน": [
      {
        "productType": 1,
        "current": [
          {
            "labels": [
              1782100800000,
              1782619200000
            ],
            "value": 0
          },
          {
            "labels": [
              1782705600000,
              1783224000000
            ],
            "value": 0
          },
          "... เหลืออีก 6 รายการ"
        ],
        "lastYear": [
          {
            "labels": [
              1750564800000,
              1751083200000
            ],
            "value": 0
          },
          {
            "labels": [
              1751169600000,
              1751688000000
            ],
            "value": 0
          },
          "... เหลืออีก 6 รายการ"
        ]
      },
      {
        "productType": 2,
        "current": [
          {
            "labels": [
              1782100800000,
              1782619200000
            ],
            "value": 0
          },
          {
            "labels": [
              1782705600000,
              1783224000000
            ],
            "value": 0
          },
          "... เหลืออีก 6 รายการ"
        ],
        "lastYear": [
          {
            "labels": [
              1750564800000,
              1751083200000
            ],
            "value": 0
          },
          {
            "labels": [
              1751169600000,
              1751688000000
            ],
            "value": 0
          },
          "... เหลืออีก 6 รายการ"
        ]
      },
      "... เหลืออีก 1 รายการ"
    ],
    "เกม": [
      {
        "productType": 1,
        "current": [
          {
            "labels": [
              1772337600000,
              1774929600000
            ],
            "value": 0
          },
          {
            "labels": [
              1775016000000,
              1777521600000
            ],
            "value": 0
          },
          "... เหลืออีก 4 รายการ"
        ],
        "lastYear": [
          {
            "labels": [
              1740801600000,
              1743393600000
            ],
            "value": 0
          },
          {
            "labels": [
              1743480000000,
              1745985600000
            ],
            "value": 0
          },
          "... เหลืออีก 4 รายการ"
        ]
      },
      {
        "productType": 2,
        "current": [
          {
            "labels": [
... (ตัดให้สั้น)

POST Statement

/report/statement

Description

งบรายวันแยกตามผลิตภัณฑ์ พร้อมรายการโอนเครดิตในช่วงเดียวกัน ดึงมาคู่กันเพราะพอร์ทัลใช้คู่กันเสมอ

Request Body

{
  "customerId": 9000001,
  "from": "2026-07-22",
  "to": "2026-07-29"
}

Response Example

{
  "data": {
    "statement": [
      {
        "date": 1784779200000,
        "type": 99,
        "product": 1,
        "betting": 273139.75,
        "commission": -18654.55,
        "cashBalance": 254485.2,
        "totalBalance": -152000.69
      }
    ],
    "transfers": [
      {
        "winLostDate": 1784779200000,
        "transDesc": "To Customer:abc88(...)",
        "amount": -0.74,
        "transDate": 1784810147857
      }
    ]
  }
}

แถว type: 100 เป็นแถวยอดยกมา ซึ่ง product, betting, commission จะเป็น null

POST Calendar

/report/calendar

Description

ข้อมูลประกอบตัวเลือกวันที่ — วันไหนมีบิล และวันไหน upstream ปิดยอดแล้ว คืนมาเป็น "YYYY-MM-DD" ทั้งหมด ไม่ต้องแปลง epoch เอง

Request Body

{
  "customerId": 9000001,
  "from": "2026-07-01",
  "to": "2026-07-29",
  "useUsername": "optional"
}

customerId จำเป็น — ส่งเป็น request body ตรง ๆ ไม่ต้องห่อด้วย data (บล็อก data ด้านล่างเป็น รูปแบบ response ที่ได้กลับมา)

Response Example

{
  "data": {
    "datesWithBet": ["2026-07-28", "2026-07-29"],
    "settledDays": {
      "2026-07-27": 1,
      "2026-07-28": 1,
      "2026-07-29": 0
    }
  }
}

settledDays: 1 = ปิดยอดแล้ว, 0 = ยังไม่ปิด — วันที่ยังไม่ปิด ตัวเลขในรายงานยังเปลี่ยนได้ ควรเตือนผู้ใช้ก่อนเอายอดไปใช้จริง

POST Settled Bets

/report/bets

Description

บิลรายใบที่เคลียร์แล้ว แยกตามผลิตภัณฑ์ (sports, eSports, games, liveCasino, raceTote) พร้อมจำนวนบิลต่อผลิตภัณฑ์ในฟิลด์ counts

Request Body

{
  "customerId": 9100002,
  "from": "2026-07-28",
  "language": "1"
}

โครงสร้างบิลลึกและต่างกันตามผลิตภัณฑ์ จึงส่งผ่านตามที่ upstream คืนมาโดยไม่ปรับรูป

Response Example

{
  "count": 0,
  "counts": {
    "eSports": 0,
    "games": 0,
    "liveCasino": 0,
    "raceTote": 0,
    "sports": 0
  },
  "customerId": 9100010,
  "data": {
    "eSports": [],
    "games": [],
    "liveCasino": [],
    "raceTote": [],
    "sports": []
  },
  "from": "2026-08-01",
  "groups": null,
  "rows": null,
  "success": true,
  "to": "2026-08-13",
  "totals": {},
  "usedUsername": "agentdemo168"
}

POST Running Bets

/report/bets/running

Description

บิลของลูกค้าที่ ยังไม่จบ / รอผล (running) ต่อ customerId รายคน — เรียกพอร์ทัล /betlist/Running ไม่มีช่วงวันที่ (บิลที่ยังไม่ settle ไม่มีวันจบ) ต่างจาก /report/outstanding ที่เป็นยอด exposure รวมทั้งเอเย่นต์ ไม่แยกรายลูกค้า

ต้องเรียกด้วยเอเย่นต์ที่เป็น เจ้าของสมาชิกคนนั้น (ตัวเดียวกับตอนสมัคร / ค่า usedUsername ที่ได้จาก /create) มิฉะนั้นพอร์ทัลตอบ 403

Request Body

{
  "customerId": 37485351,
  "filters": [1],
  "language": "1",
  "useUsername": "zum45agent"
}

customerId จำเป็น · filters ไม่ใส่ = ค่าเริ่มต้นชุดเดียวกับ Settled Bets · accountId / useUsername ใช้เลือก/สลับบัญชีเหมือนเส้นรายงานอื่น

Response Example

{
  "success": true,
  "customerId": 37485351,
  "count": 0,
  "counts": {
    "eSports": 0,
    "games": 0,
    "liveCasino": 0,
    "raceTote": 0,
    "sports": 0
  },
  "totals": {},
  "rows": null,
  "groups": null,
  "data": {
    "eSports": [],
    "games": [],
    "liveCasino": [],
    "raceTote": [],
    "sports": []
  },
  "usedUsername": "zum45agent"
}

POST Breakdown

/report/breakdown

Description

รวม billing breakdown และ cancelled-campaign breakdown ไว้ในคำขอเดียว

Request Body

{
  "customerId": 9000001,
  "from": "2026-07-01",
  "to": "2026-07-29",
  "useUsername": "optional"
}

customerId จำเป็น

หมายเหตุ

ทั้งสองเส้นคืน array ว่างในทุก response ที่เก็บได้จริง จึงส่งผ่านแบบไม่กำหนดชนิดข้อมูล แทนที่จะเดาโครงสร้างขึ้นมาเอง

Response Example

{
  "customerId": 9100010,
  "data": {
    "billing": [],
    "cancelled": []
  },
  "errors": null,
  "from": "2026-08-01",
  "success": true,
  "to": "2026-08-13",
  "usedUsername": "agentdemo168"
}

POST Outstanding Bets

/report/outstanding

Description

ความเสี่ยงที่ยังไม่เคลียร์ แยกรายแมตช์ — มีเงินวางอยู่ข้างไหนเท่าไหร่ ทั้งเต็มเวลาและครึ่งแรก เรียงจากยอดค้างมากสุดลงมาให้แล้ว เพราะเป็นหน้าจอความเสี่ยง แมตช์ที่เงินเยอะสุดต้องเห็นก่อน

Request Body

{
  "monitored": false,
  "language": "1"
}

ทุกฟิลด์เว้นได้หมด — monitored: true จะกรองเหลือเฉพาะแมตช์ที่ตั้งเฝ้าไว้

Response Example

{
  "count": 112,
  "liveCount": 78,
  "totalExposure": 175286.18,
  "data": [
    {
      "leagueName": "Sydney Super Cup",
      "sportName": "Football",
      "homeTeamName": "Sydney FC",
      "awayTeamName": "Tottenham Hotspur",
      "isLive": false,
      "liveHomeScore": 0,
      "showTime": 1785318300000,
      "fullTime": {
        "home": 4.8, "away": 0,
        "hdpTotal": 4.8, "hdpTotalCount": 1,
        "over": 1205, "under": 0,
        "ouTotal": 1205, "ouTotalCount": 2
      },
      "halfTime": { "hdpTotal": 0, "ouTotal": 0 }
    }
  ]
}

totalExposure = fullTime.hdpTotal + fullTime.ouTotal + halfTime.hdpTotal + halfTime.ouTotal รวมทุกแมตช์ — คำนวณมาให้แล้วไม่ต้องบวกเอง

POST Credit Downline

/report/credit

Description

ฐานะเครดิตของตัวเองพร้อมของดาวน์ไลน์ทุกคน รวม 3 เส้นเป็นคำขอเดียว (creditbalance/self, creditbalance/children, BalanceInfo)

Request Body

{
  "customerId": 9000001,
  "accountStatus": 0
}

accountStatus — 0 = ทุกสถานะ, 1 = เฉพาะที่ใช้งานอยู่ ค่าเริ่มต้นคือ 0 (ส่ง 0 มาเองก็ยังเป็น 0 ไม่ถูกแทนที่)

Response Example

{
  "data": {
    "self": {
      "customerId": 9000001,
      "customerInfo": { "accountId": "abc", "accountStatus": 1 },
      "credit": { "givenCredit": 50000000, "usedCredit": 50000000 },
      "balance": { "totalBalance": -441911.43 },
      "outstanding": { "totalOutstanding": 299643.86 },
      "betCredit": 4723897218.11
    },
    "children": [ "... 34 รายการ รูปแบบเดียวกับ self" ]
  }
}

POST IP Groups

/report/ipgroups

Description

ยอดเดิมพันรวมตาม IP ต้นทาง — ใช้จับกรณีหลายสมาชิกยิงจาก IP เดียวกัน ซึ่งเป็นเหตุผลที่รายงานนี้มีอยู่ เรียงตามจำนวนสมาชิกมากสุดก่อน แล้วค่อยตามยอดเดิมพัน

Request Body

{
  "filter": 1
}

filter เป็น ตัวเลขเดี่ยว (1 = กีฬา, 2 = คาสิโน, 3 = เกม) ไม่ใช่ array แบบเส้นอื่น เพราะ upstream รับ filters แค่ค่าเดียวที่เส้นนี้ ค่าเริ่มต้นคือ 1

Response Example

{
  "count": 421,
  "sharedIPs": 1,
  "totalStake": 500000,
  "data": [
    {
      "ip": "203.0.113.58",
      "memberCount": 2,
      "betCount": 6,
      "memberTotal": 7500,
      "agentTotal": 0,
      "maTotal": 0,
      "smaTotal": 6000
    }
  ]
}

sharedIPs = จำนวน IP ที่มี memberCount > 1 — คือตัวเลขที่ควรดูก่อน ส่วน IP ที่มีสมาชิกคนเดียวเป็นเรื่องปกติ

POST Directory

/report/directory

Description

รายชื่อบัญชีที่ใช้ทำรายงานได้ — ดาวน์ไลน์ชั้นแรก และบัญชี agent ที่สลับไปได้

Request Body

{
  "useUsername": "optional"
}

ไม่มีพารามิเตอร์ที่จำเป็น — ทำงานบนโทเคนของล็อกอินเอง useUsername ใส่เมื่อต้องการระบุบัญชีที่ใช้โทเคน (บล็อก data ด้านล่างเป็น รูปแบบ response)

Response Example

{
  "data": {
    "linkedAgents": [
      { "customerId": 9000001, "username": "abc", "isLocked": false }
    ],
    "children": [
      { "customerId": 9100005, "accountId": "abc08", "firstName": "P.BOY" }
    ]
  }
}

เรียกเส้นนี้ก่อนเสมอ — username ใช้เป็น accountId ของ dashboard/metrics ได้ตรงๆ และ customerId ใช้เป็น requesterId ของรายงานได้ตรงๆ จึงไม่ต้องให้ผู้ใช้พิมพ์เอง