The Seamless (single wallet) test suite contains 12 test cases, executed by the TestKit in the selected order.
Merchant callbacks support two request formats. Use the switch above to view the request examples for your integration — JSON is shown by default. Responses and assertions are identical in both formats.
| Step type | Direction | Endpoint |
|---|---|---|
login | TestKit → Yeebet platform (STG) | {SGO_API_URL}/api/login (POST, application/json body) |
balance / deduct / deposit / rollback | TestKit (acting as the platform) → merchant callbacks | The Balance / Deduct / Deposit / Rollback URLs configured on the page |
Request format (JSON mode). Every callback is a POST with Content-Type: application/json and a JSON body. The bets field is a bare JSON object when the step carries a single bet ("bets": {...}) and a JSON array when a notification carries multiple bets. Optionally, the page toggle “Send bets as JSON string” (off by default) transmits the same shape as a JSON string (e.g. "{\"gameid\":...}") so the merchant must JSON-parse it again. The signature is unaffected either way (bets is excluded from signing). When a token is configured, every callback request also carries the token field (which is included in the signature); the examples below use token=mock_123. The login request carries the same configured token as its optional token parameter (Company Session Token).
Request format (Form mode). Callbacks are POST with Content-Type: application/x-www-form-urlencoded, except balance, which becomes a GET with the parameters in the query string. Form values are strings, so bets is serialized to a JSON string with the same shape as in JSON mode — an object string (bets={...}) for a single bet, an array string (bets=[{...},...]) when a notification carries multiple bets — and the merchant must JSON-parse it again. Form mode never sends a token field. The form examples below show the decoded field values; on the wire they are URL-encoded.
Signature (identical in both formats). Every request carries a sign parameter (MD5): lowercase the key names, drop empty values plus the sign and bets fields, sort keys in ascending ASCII order, join as k=v&...&key={secretKey} (values are not URL-encoded), then take the MD5 hex digest. The login request is signed with the Login Secret Key configured in the Login section; wallet callbacks are signed with the Callback Secret Key.
• Each step asserts Number(response.result) === expected_result_code (every step in this suite expects 0), except the duplicate / unknown notification steps (003 step 2, 005 step 3, 010 step 3, 011 step 1): there the merchant may answer 0 or reject the resend with a negative code (e.g. -1000 SameIdAlreadyProcessed) — either passes as long as the balance check below shows no movement.
• For steps with an expected balance change, the executor calls the merchant Balance endpoint before and after the step and asserts the delta (tolerance 0.005).
• login additionally requires a non-empty openurl; balance additionally requires a numeric balance field.
• All steps within one test case share the same serialnumber (they refer to the same bet). For a single bet, bets.id and bets.serialnumber equal the top-level serialnumber (numeric); multi-bets each carry their own. gameroundno/gameno share one per-case number and gameroundid is an independent number. createtime/settletime are Unix seconds, frozen per case (duplicate resends are byte-identical). bets.currency is always present and equals the page currency; bets.gameid is the gameid configured on the page (the same value sent as gid at login). Field types inside bets: id, serialnumber, gameroundno, gameno, gameroundid, createtime, settletime are integers; cid is a string; bets.username is the page username.
• Top-level type and amount are strings in both modes ("type": "1", "amount": "-1.00"); the signature is computed over the same text (amount=-1.00&...&type=1).
• Every notification carries a fresh notifyid. Duplicate-notification cases (the second send in 004, 006, and 011) resend the same notifyid.
• A rollback notification re-sends the notifyid of the request it rolls back, together with the same type and serialnumber (doc 2.6.5 / 2.7.4 note II: the merchant verifies notifyid + type + serialnumber). Only 011 (unknown serialnumber, no original request) carries a fresh notifyid.
All examples below use appid=your_appid, username=player01, currency=THB; sign and the id fields are placeholders. A full request body is shown only the first time it appears; later cases describe the differences.
Verify that /api/login returns the game lobby link. If login fails, subsequent tests may also fail. The login request is the same in both formats. If a Token is configured, it is sent as the optional token parameter (Company Session Token) and echoed back in JSON-format wallet callbacks. The gid parameter carries the gameid configured on the page (required there) so the launch enters that specific game; the same gameid is used as bets[].gameid in Deduct/Deposit callbacks.
{
"appid": "your_appid",
"username": "player01",
"iscreate": "1",
"clienttype": "1",
"language": "2",
"currency": "THB",
"gid": "101",
"token": "mock_123",
"sign": "0d8f2e0b8a5c4f1e9c7a6b5d4e3f2a1b"
}
{
"result": 0,
"openurl": "https://stg-lobby.example.com/?token=xxxx"
}
The platform calls the merchant Balance endpoint; expect result = 0 and a numeric balance.
{
"appid": "your_appid",
"username": "player01",
"token": "mock_123",
"notifyid": "25081111203512345",
"sign": "0d8f2e0b8a5c4f1e9c7a6b5d4e3f2a1b"
}
GET {Balance_url}?appid=your_appid&username=player01¬ifyid=25081111203512345&sign=0d8f2e0b8a5c4f1e9c7a6b5d4e3f2a1b
{
"result": 0,
"balance": 100.00
}
The same deduct notification is sent twice (same serialnumber and type). The merchant must not deduct twice (idempotency). The second response may be result = 0 or a negative rejection code; the pass criterion is that the balance does not move again.
| Step | Endpoint | type | amount | Expected result | Balance change |
|---|---|---|---|---|---|
| 1 | Deduct | 1 | -1.00 | 0 | -1.00 |
| 2 | Deduct | 1 | -1.00 | 0 or negative (rejection) | 0 (no second deduction) |
{
"appid": "your_appid",
"username": "player01",
"token": "mock_123",
"notifyid": "25081111203512345",
"type": "1",
"amount": "-1.00",
"currency": "THB",
"serialnumber": "1754882501123456",
"bets": {
"gameid": 101,
"createtime": 1756090000,
"serialnumber": 1754882501123456,
"userstatus": 0,
"betpoint": "PLAYER",
"betodds": 2.0,
"userid": 100001,
"username": "player01",
"commamount": 0.0,
"gameroundid": 1754882501123900,
"settletime": 1756090000,
"gameresult": "",
"closeRound": false,
"winlost": 0.0,
"gametype": 4,
"gamestyle": 1,
"currency": "THB",
"id": 1754882501123456,
"gameroundno": 1754882501123800,
"state": 0,
"gameno": 1754882501123800,
"bettype": 0,
"cid": "338891",
"betamount": 1.0,
"foregift": 0.0
},
"sign": "0d8f2e0b8a5c4f1e9c7a6b5d4e3f2a1b"
}
appid=your_appid
username=player01
notifyid=25081111203512345
type=1
amount=-1.00
currency=THB
serialnumber=1754882501123456
bets={"gameid":101,"createtime":1756090000,"serialnumber":1754882501123456,"userstatus":0,"betpoint":"PLAYER","betodds":2.0,"userid":100001,"username":"player01","commamount":0.0,"gameroundid":1754882501123900,"settletime":1756090000,"gameresult":"","closeRound":false,"winlost":0.0,"gametype":4,"gamestyle":1,"currency":"THB","id":1754882501123456,"gameroundno":1754882501123800,"state":0,"gameno":1754882501123800,"bettype":0,"cid":"338891","betamount":1.0,"foregift":0.0}
sign=0d8f2e0b8a5c4f1e9c7a6b5d4e3f2a1b
{
"result": 0,
"balance": 99.00,
"orderno": "M20260811001"
}
Step 2 is an exact resend of step 1 (same notifyid, serialnumber, and payload); expected response is the same with result = 0 and no second deduction.
A bet deduct (type=1) followed by a settle deposit (type=9) for the same serialnumber.
| Step | Endpoint | type | amount | Expected result | Balance change |
|---|---|---|---|---|---|
| 1 | Deduct | 1 | -1.00 | 0 | -1.00 |
| 2 | Deposit | 9 | +2.00 | 0 | +2.00 |
{
"appid": "your_appid",
"username": "player01",
"token": "mock_123",
"notifyid": "25081111203698765",
"type": "9",
"amount": "2.00",
"currency": "THB",
"serialnumber": "1754882501123456",
"bets": {
"gameid": 101,
"createtime": 1756090000,
"serialnumber": 1754882501123456,
"userstatus": 0,
"betpoint": "PLAYER",
"betodds": 2.0,
"userid": 100001,
"username": "player01",
"commamount": 1.0,
"gameroundid": 1754882501123900,
"settletime": 1756090000,
"gameresult": "",
"closeRound": true,
"winlost": 1.0,
"gametype": 4,
"gamestyle": 1,
"currency": "THB",
"id": 1754882501123456,
"gameroundno": 1754882501123800,
"state": 1,
"gameno": 1754882501123800,
"bettype": 0,
"cid": "338891",
"betamount": 1.0,
"foregift": 0.0
},
"sign": "0d8f2e0b8a5c4f1e9c7a6b5d4e3f2a1b"
}
appid=your_appid
username=player01
notifyid=25081111203698765
type=9
amount=2.00
currency=THB
serialnumber=1754882501123456
bets={"gameid":101,"createtime":1756090000,"serialnumber":1754882501123456,"userstatus":0,"betpoint":"PLAYER","betodds":2.0,"userid":100001,"username":"player01","commamount":1.0,"gameroundid":1754882501123900,"settletime":1756090000,"gameresult":"","closeRound":true,"winlost":1.0,"gametype":4,"gamestyle":1,"currency":"THB","id":1754882501123456,"gameroundno":1754882501123800,"state":1,"gameno":1754882501123800,"bettype":0,"cid":"338891","betamount":1.0,"foregift":0.0}
sign=0d8f2e0b8a5c4f1e9c7a6b5d4e3f2a1b
{
"result": 0,
"balance": 101.00,
"orderno": "M20260811002"
}
The same settle deposit notification is sent twice (same serialnumber and type). The merchant must not credit the winnings twice (idempotency). The second settle response may be result = 0 or a negative rejection code; the pass criterion is that the balance does not move again.
| Step | Endpoint | type | amount | Expected result | Balance change |
|---|---|---|---|---|---|
| 1 | Deduct | 1 | -1.00 | 0 | -1.00 |
| 2 | Deposit | 9 | +2.00 | 0 | +2.00 |
| 3 | Deposit | 9 | +2.00 | 0 or negative (rejection) | 0 (no second credit) |
Request/response are identical to 004; step 3 is an exact resend of step 2.
A bet deduct followed by a void-bet deposit (type=7) returning the stake.
| Step | Endpoint | type | amount | Expected result | Balance change |
|---|---|---|---|---|---|
| 1 | Deduct | 1 | -1.00 | 0 | -1.00 |
| 2 | Deposit | 7 | +1.00 | 0 | +1.00 |
Step 2 has the same structure as the Deposit in 004, with these differences: type="7", amount="1.00", and the bet carries winlost="1.00", commamount="1.00", state=1. The response format is the same as Deposit.
Bet deduct, settle deposit, then a re-settle correction deposit (type=10) for the same bet.
| Step | Endpoint | type | amount | Expected result | Balance change |
|---|---|---|---|---|---|
| 1 | Deduct | 1 | -1.00 | 0 | -1.00 |
| 2 | Deposit | 9 | +2.00 | 0 | +2.00 |
| 3 | Deposit | 10 | +0.50 | 0 | +0.50 |
Step 3 has the same structure as Deposit, with these differences: type="10", amount="0.50", and the bet carries winlost="1.50", state=2. The response format is the same as Deposit.
A deduct notification is assumed lost, so the platform sends a rollback (type=1). The merchant must return the deducted amount and respond result = 0.
| Step | Endpoint | type | amount | Expected result | Balance change |
|---|---|---|---|---|---|
| 1 | Deduct | 1 | -1.00 | 0 | -1.00 |
| 2 | Rollback | 1 | +1.00 | 0 | +1.00 |
{
"appid": "your_appid",
"username": "player01",
"token": "mock_123",
"notifyid": "25081111204055555",
"type": "1",
"amount": "1.00",
"serialnumber": "1754882501123456",
"errmsg": "timeout",
"sign": "0d8f2e0b8a5c4f1e9c7a6b5d4e3f2a1b"
}
appid=your_appid
username=player01
notifyid=25081111204055555
type=1
amount=1.00
serialnumber=1754882501123456
errmsg=timeout
sign=0d8f2e0b8a5c4f1e9c7a6b5d4e3f2a1b
{
"result": 0,
"balance": 100.00
}
A settle deposit notification is assumed lost, so the platform sends a rollback (type=9). The merchant must remove the deposited amount and respond result = 0.
| Step | Endpoint | type | amount | Expected result | Balance change |
|---|---|---|---|---|---|
| 1 | Deduct | 1 | -1.00 | 0 | -1.00 |
| 2 | Deposit | 9 | +2.00 | 0 | +2.00 |
| 3 | Rollback | 9 | 2.00 | 0 | -2.00 |
Step 3 has the same structure as the Rollback in 008, with these differences: type="9", amount="2.00". The response format is the same.
The same rollback notification is sent twice (same notifyid, type and serialnumber). The merchant must only return the deducted amount once (idempotency). The second rollback response may be result = 0 or a negative rejection code; the pass criterion is that the balance does not move again.
| Step | Endpoint | type | amount | Expected result | Balance change |
|---|---|---|---|---|---|
| 1 | Deduct | 1 | -1.00 | 0 | -1.00 |
| 2 | Rollback | 1 | +1.00 | 0 | +1.00 |
| 3 | Rollback | 1 | +1.00 | 0 or negative (rejection) | 0 (no second rollback) |
Request/response are identical to 008; step 3 is an exact resend of step 2.
A rollback for a serialnumber the merchant never processed. The merchant must not move the balance; per the spec it responds result = 0, but a negative rejection code (e.g. order not found) is accepted as well. The pass criterion is that the balance does not change.
| Step | Endpoint | type | amount | Expected result | Balance change |
|---|---|---|---|---|---|
| 1 | Rollback | 1 | 1.00 | 0 or negative (rejection) | 0 (no balance movement) |
The request has the same structure as the Rollback in 008 (with a brand-new serialnumber).
{
"result": 0,
"balance": 100.00
}