Merchant Integration Guide
เอกสารสำหรับ merchant ที่ต้องการเชื่อมต่อกับ GameZero แบบ Seamless Wallet — เงินของผู้เล่นอยู่ในระบบของคุณตลอด ไม่มีการโอนเข้า/ออก ทุกครั้งที่ผู้เล่นเดิมพันหรือรับรางวัล ระบบเราจะเรียก endpoint กระเป๋าเงินของคุณแบบเรียลไทม์
ภาพรวมการทำงาน
ผู้เล่นกดเล่นเกม
│
├─ 1) คุณเรียก POST /v1/launch → ได้ game_url ส่งผู้เล่นเข้าเกม
│
เกมเริ่มเดิน (ผู้เล่นแทง / ได้รางวัล / โดนยกเลิก)
│
└─ 2) เราเรียก POST {your_wallet_url} → action = bet | settle | cancel | balance
คุณขยับยอดเงินแล้วตอบ balance กลับ
การยืนยันตัวตน (HMAC Signature)
ทั้งสองขาใช้สูตร signature เดียวกัน ต่างกันแค่ key ที่ใช้:
signature = hex( HMAC_SHA256( secret, timestamp + "." + rawBody ) )
| ขา | secret ที่ใช้ | ใครตรวจ |
|---|---|---|
| คุณ → เรา | API Secret (ออกให้ในหน้า Admin) | gateway ของเรา |
| เรา → คุณ | Wallet Secret (ตั้งร่วมกันตอนเปิดบัญชี) | ระบบของคุณ |
ทุก request แนบ 3 header:
| Header | ค่า |
|---|---|
X-API-Key | API key ของคุณ (เฉพาะขาที่คุณเรียกเรา) |
X-Timestamp | Unix timestamp (วินาที) — คลาดเคลื่อนได้ไม่เกิน 5 นาที ไม่งั้นถือว่า replay |
X-Signature | ผลลัพธ์จากสูตรด้านบน |
Environment & Base URL
| Environment | Base URL (gateway) |
|---|---|
| Staging | https://api.gamezero-tech.com (API key แบบ staging) |
| Production | https://api.gamezero-tech.com (API key แบบ production) |
environment ถูกกำหนดโดย API key ที่ใช้ ไม่ใช่ URL — key คนละชุดคนละ environment
สร้างผู้เล่น
POST/v1/players
{ "username": "member_test" }
Idempotent — เรียกซ้ำด้วย username เดิมได้ ระบบจะผูก player_prefix ของคุณให้เอง (เช่น m001_member_test) ก่อน launch ครั้งแรกต้องมีผู้เล่นก่อน
รายการเกม
GET/v1/games — คืนเฉพาะค่าย/เกมที่เปิดให้ merchant คุณ
{ "status":"ok", "games":[
{"provider":"allbet","game_code":"0","name":"AllBet Live Casino","category":"live"},
{"provider":"m8","game_code":"SPORTS","name":"M8 Sportsbook","category":"sport"}
]}
เปิดเกม (Launch)
POST/v1/launch
{ "username":"member_test", "provider":"allbet",
"game_code":"0", "lang":"th", "return_url":"https://your-site.com/lobby" }
{ "status":"ok",
"game_url":"https://...", // redirect ผู้เล่นไปที่นี่
"session_token":"..." } // เราผูก token นี้กับผู้เล่น ใช้ตอน callback
ดูยอดเงิน
GET/v1/players/{username}/balance — เราจะไปถามกระเป๋าเงินของคุณให้
รายการเดินเงิน
GET/v1/transactions?from=&to=&provider=&type=&limit=
GET/v1/players/{username}/statement
โปรโตคอล Wallet Callback (เราเรียกคุณ)
คุณเปิด endpoint 1 ตัว (เช่น POST https://your-site.com/wallet) แล้วแจ้ง URL นี้ให้เราตอนเปิดบัญชี ทุก request เป็น JSON โครงเดียวกัน:
{
"action": "bet", // balance | bet | settle | cancel
"tx_id": "6f1c...", // ★ unique ต่อ 1 การขยับเงิน ใช้กันซ้ำ
"round_id": "512345678", // รหัสรอบเกม (อ้างอิงกลุ่ม bet/settle/cancel เดียวกัน)
"provider": "allbet",
"username": "member_test",
"game_code": "0",
"amount": "10.50", // เป็นบวกเสมอ (ทิศทางดูจาก action)
"timestamp": 1720000000
}
ตอบกลับ (HTTP 200 เสมอ แม้ error ทางธุรกิจ):
// สำเร็จ
{ "status":"ok", "balance_before":"1000.00", "balance":"989.50" }
// ซ้ำ (tx_id เดิม) — ต้องคืน balance เดิม ห้ามขยับเงิน
{ "status":"ok", "balance":"989.50", "duplicate":true }
// ผิดพลาด
{ "status":"error", "code":"insufficient_funds", "message":"not enough balance" }
action = balance
ถามยอดปัจจุบันของผู้เล่น ไม่ขยับเงิน ตอบ {"status":"ok","balance":"..."}
action = bet
ตัดเงิน เท่ากับ amount ถ้าเงินไม่พอ ตอบ insufficient_funds ห้ามตัดติดลบ
action = settle
จ่ายรางวัล โดยบวก amount เข้ากระเป๋า (amount = ยอดที่ต้องจ่าย ไม่ใช่กำไรสุทธิ) ค่ายที่แยก bet/settle จะยิง 2 ครั้งใน round เดียวกัน
action = cancel — อ่านให้ละเอียด
ยกเลิก/คืนเงินของ bet เดิม โดยบวก amount (เท่ายอด bet ที่เคยตัด) กลับเข้ากระเป๋า
- ต้องอ้างอิงให้ตรง bet เดิม — เราส่ง
round_idชุดเดียวกับ bet ที่จะยกเลิกมาให้ ใช้จับคู่ในระบบคุณ ห้ามคืนมั่ว - cancel เป็น tx_id ใหม่ (คนละตัวกับ bet) แต่ผูกกับ round เดิม — ถ้าคุณ dedup ผิดชั้น อาจคืนซ้ำหรือไม่คืนเลย
- cancel ต้อง idempotent เช่นกัน — ถ้าเรายิง cancel ซ้ำ (เพราะ timeout แล้ว retry) ต้องคืน balance เดิม ไม่บวกเงินซ้ำ
- ถ้าไม่เคยเห็น bet นั้น (bet มาไม่ถึง/หาย) ให้ตอบสำเร็จแบบ no-op หรือ
round_not_foundตามนโยบายคุณ — อย่าบวกเงินลอย
ฝั่งเราเช็คก่อนอยู่แล้วว่า bet เดิมต้องมีจริง (ผ่าน dedup key provider_id + provider_tx_id + type) และถ้า cancel ซ้ำเราจะส่ง duplicate กลับให้ค่าย แต่ฝั่งคุณก็ต้องกันซ้ำด้วยเพราะเป็นด่านสุดท้ายของเงินจริง
Idempotency (กันเงินซ้ำ)
กุญแจคือ tx_id: ทุก action ที่ขยับเงินมี tx_id ไม่ซ้ำ ให้คุณเก็บ tx_id ที่เคยประมวลผลไว้ แล้ว:
ได้รับ wallet request:
ถ้า tx_id เคยเห็นแล้ว → คืน balance ที่บันทึกไว้ตอนนั้น + duplicate:true (ห้ามขยับเงิน)
ถ้ายังไม่เคยเห็น → ขยับเงิน, บันทึก (tx_id → balance หลังทำ) ในทรานแซกชันเดียวกัน
Race condition & Queue — หัวใจของความถูกต้อง
บางค่ายยิง callback ของผู้เล่นคนเดียวกัน พร้อมกันหลายเส้น หรือส่ง bet กับ settle ไล่กันเร็วมากจนมาถึงคุณเกือบพร้อมกัน ถ้าคุณอ่าน balance → คำนวณ → เขียนกลับ แบบไม่ล็อก จะเกิด lost update (เงินหาย/เกิน)
กติกาบังคับฝั่ง merchant
- ต้อง serialize ต่อผู้เล่น — คำสั่งที่ขยับเงินของ
usernameเดียวกันต้องทำทีละอันเท่านั้น
เลือกวิธีใดวิธีหนึ่ง:
| วิธี | ทำยังไง | เหมาะกับ |
|---|---|---|
| Row lock ใน DB | SELECT ... FOR UPDATE ที่แถว wallet ของ user ก่อนคำนวณ แล้ว commit | ง่ายสุด ทำได้เลยด้วย SQL |
| Atomic update | UPDATE wallet SET bal = bal - :amt WHERE user=:u AND bal >= :amt เช็คแถวที่กระทบ | bet เดี่ยวๆ เร็ว |
| Per-user queue | เข้าคิวตาม hash(username) → worker เดียวต่อ user | ค่ายที่ยิงถี่/เป็นชุด |
- เข้าคิว ต่อผู้เล่น (partition by username) ประมวลผลเรียงตามลำดับที่รับเข้ามา — worker 1 ตัวต่อ 1 user ห้ามขนานภายใน user เดียว
- ถ้า settle อ้าง round ที่ยังไม่มี bet ให้ พักไว้แล้ว retry (หรือ hold ในคิวสั้นๆ) อย่าเพิ่งปฏิเสธทันที เพราะ bet อาจตามมาในเสี้ยววินาที
- ยังต้อง idempotent ด้วย tx_id เสมอ — queue ช่วยเรื่องลำดับ แต่ retry ยังทำให้ของซ้ำได้
(provider_id, provider_tx_id, type) และ settle/cancel ที่ยิงไม่ถึงคุณจะถูกเรา retry แบบ exponential backoff อัตโนมัติ — แต่ด่านเงินจริงคือกระเป๋าคุณ จึงต้องกันซ้ำ+เรียงลำดับที่ฝั่งคุณด้วยเสมอกติกา Cancel แยกตามลักษณะค่าย
| รูปแบบค่าย | พฤติกรรม cancel ที่คุณต้องรองรับ |
|---|---|
| bet/settle แยกจังหวะ (เช่น AllBet) | cancel อ้าง bet ด้วย round_id เดิม → คืนยอด bet. ถ้า settle ไปแล้วอาจมี cancel ตามมาเพื่อ reverse ทั้งรอบ |
| bet+settle รวมจังหวะเดียว | cancel = reverse ทั้งก้อน (คืน bet, หัก payout ที่จ่ายไป) — จับคู่ด้วย round_id |
| กีฬา/โพลผลทีหลัง (เช่น M8) | ไม่มี settle แบบ push — เราดึงผลเองแล้วส่ง settle/cancel มาให้ทีหลัง อาจห่างจาก bet เป็นชั่วโมง คุณต้องหา bet เดิมด้วย round_id ให้เจอแม้เวลาผ่านไปนาน |
สรุปหลักการเดียวที่ใช้ได้ทุกค่าย: จับคู่ด้วย round_id, กันซ้ำด้วย tx_id, ขยับเงินใต้ล็อกต่อ user
Error codes ที่คุณควรตอบกลับ
| code | เมื่อไหร่ |
|---|---|
insufficient_funds | ยอดไม่พอตัด bet |
invalid_parameter | amount/field ผิดรูปแบบ |
round_not_found | cancel/settle อ้าง round ที่ไม่มี |
bad_signature | signature ไม่ผ่าน |
Checklist ก่อนขึ้นจริง
[ ] ตรวจ signature ทุก wallet request (secret + skew 5 นาที)
[ ] balance / bet / settle / cancel ครบ และตอบ HTTP 200 เสมอ
[ ] tx_id idempotent: ซ้ำ → คืน balance เดิม + duplicate:true (บันทึก tx_id ใน tx เดียวกับการขยับเงิน)
[ ] cancel: จับคู่ round_id เดิม, idempotent, ไม่คืนเงินลอยเมื่อไม่พบ bet
[ ] serialize ต่อ username (row lock / atomic update / per-user queue)
[ ] ค่ายกลุ่ม batch (SA ฯลฯ): queue เรียงลำดับต่อ user + hold settle ที่มาก่อน bet
[ ] ทดสอบครบ: bet → settle, bet → cancel, ยิงซ้ำทุก action, ยิงพร้อมกัน 2 เส้น
มีคำถามเรื่อง integration ติดต่อทีม GameZero — เอกสารนี้อ้างอิงโปรโตคอลจริงของระบบ v1