1. Getting Started
[Game] FairPlay API
  • Getting Started
    • Introduction
    • Session and Token
    • Request and Response
    • Real-time Events (SSE)
    • Lotto Rules — หวยเฮงเฮง
    • Provably Fair
  • Player
    • Demo
      • Demo Login
    • Player Bet History
      GET
  • Common
    • Game
      • By Game Code
        • Provably Fair
          • Set Client Seed
          • Verify Result
          • Get Revealed Server Seed
        • Check Session
        • Get Balance
        • Get Provably-Fair State
      • Lobby Game List
  • Wallet
    • Wallet Status
      GET
  • Game
    • Lotto (หวยเฮงเฮง)
      • Lotto Rules
      • Rooms
        • By Room
          • Join Round
          • Place Multi-player Bet
          • Get Round Result
          • Room Event Stream
      • Tickets
        • Create Ticket
        • Inquiry My Tickets
        • Get Ticket
      • Round
        • Get My Bets
        • Get My Result
        • Get Round Provably Fair
      • Results
        • Inquiry Results
      • Get Rooms
      • Get Current Round
      • Get Pay Table
  • Appendix
    • Game List
    • Error Codes
    • Rate Limits
  • Language List
    GET
  • Ping
    GET
  • Schemas
    • DemoLoginRequest
    • DemoLoginResponse
    • DemoProfileResponse
    • ProfileResponse
    • GetHistoryResponse
    • BetRecord
    • GameInfo
    • Room
    • GetGameResponse
    • BetRequest
    • Bet
    • BetResponse
    • BetResult
    • GetProvablyFairResponse
    • SetClientSeedRequest
    • SetClientSeedResponse
    • RotateServerSeedResponse
    • VerifyResultRequest
    • VerifyResultResponse
    • GetServerSeedResponse
    • GetStatusResponse
    • JoinRoundResponse
    • PlaceMultiPlayerBetRequest
    • PlaceBetResponse
    • RoundResultResponse
    • SuccessResponse
    • ErrorResponse
  1. Getting Started

Real-time Events (SSE)

The room stream pushes round events to the client. Open it for the room the player is looking at: GET /g/{game}/rooms/{roomId}/stream, where {game} is lotto or thai-hilo.

Connecting

  • URL: GET /gapi/v1/g/{game}/rooms/{roomId}/stream?token={token}&currency={currency}, where {game} is lotto or thai-hilo. Example: new EventSource(/gapi/v1/g/${game}/rooms/${roomId}/stream?token=${encodeURIComponent(token)}).
  • EventSource cannot set headers, so pass the session token as ?token= (URL-encoded). The server reads Authorization: Bearer first and ?token= only when that header is absent, so a fetch()-based SSE client may send the header instead. Session checks are the same as on every other game route.
  • roomId is the room number (rooms[].id from GET /gapi/v1/g/thai-hilo/rooms, or roomId from the Lotto room list), not the room code.
  • currency is optional and defaults to the session currency.
  • Which rooms emit events: THAI-HILO rooms in MULTI or MULTI_CSEED mode, and every LOTTO room. A THAI-HILO SINGLE room (or a room number that does not exist) still opens a stream, but it stays silent.
  • One channel per game + room + currency + demo/real. Demo and real players never see each other's events, and a different currency is a different channel. To change room or currency, close the EventSource and open a new one.
  • Nothing is sent on connect: no snapshot and no replay of earlier events. Call Join Round (GET …/rooms/{roomId}/round) as soon as the stream opens, and again after every reconnect.
  • Rate limit GAME_ROOM_STREAM: 60 connects per 60 s per client IP, and every reconnect counts. Players behind one NAT share the budget.
  • Response headers: Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive, X-Accel-Buffering: no.
  • Any failure before streaming starts (401, 404, 400, 429, 503, 500) is a normal JSON error response with a non-200 status, not an SSE frame.
  • The session is checked only at connect; an open stream keeps delivering after the token expires. The server sets no maximum lifetime on a stream.

Frame format

  • Every frame is a single data: <json> line followed by a blank line, where is {"type": "<event name>", "data": {…event payload…}}.
  • There are NO event:, id: or retry: lines. Every frame arrives on EventSource onmessage (event type "message"): JSON.parse(e.data) and switch on .type. addEventListener('round_started', …) never fires.
  • Key order is not guaranteed: the outer frame arrives as data then type, and round_settled data keys arrive alphabetically (gameResult, playerResult, roundID, seedRevealed). Read by key, never by position.
  • The round key inside events is roundID (capital ID), as on the room endpoints; the LOTTO-only endpoints spell it roundId.
  • There is no heartbeat, ping or comment line. A quiet room sends zero bytes: a LOTTO room with no bets can be silent for a whole round (15 minutes by default). Proxies or CDNs with idle timeouts (around 100 s) may cut such a connection, which makes EventSource reconnect.
const es = new EventSource(`${base}/g/${game}/rooms/${roomId}/stream?token=${encodeURIComponent(token)}`); // game: 'lotto' | 'thai-hilo'
es.onopen = () => loadCurrentRound();           // nothing is replayed — always re-read the round
es.onmessage = (e) => {
  const { type, data } = JSON.parse(e.data);
  switch (type) {
    case 'round_started':     /* Thai Hilo */ break;
    case 'bet_placed':        /* Thai Hilo */ break;
    case 'player_joined':     /* Thai Hilo */ break;
    case 'seed_slot_updated': /* both */ break;
    case 'round_settled':     /* both */ break;
  }
};

Round lifecycle

StatusMeaning
waitingNo betting or rolling round exists for this game + room + currency + demo/real. Only Join Round returns it (never stored). Seen before the server has opened a round, while the game is under maintenance, or between a cancelled round and its replacement (opened within about 60 s).
bettingOpen for bets until the server closes the round (up to ~2 s after endTime). Seen in Join Round and …/result.
rollingBetting closed, settlement and payouts in progress. Seen in Join Round and …/result. A round stuck here for more than 60 s is picked up by the server's recovery.
settledSeed revealed and payouts done, also for rounds with zero bets. Seen in …/result.
cancelledSettlement failed; stakes refunded (or the refund queued). Seen in …/result; no SSE event is sent and a replacement round is opened.

Timing

  • Round length is the room's roundDurationSec; when it is 0 the server uses 30 s for THAI-HILO and 900 s for LOTTO.
  • The server checks every 2 s and closes a betting round (to rolling) once its endTime has passed, so the real close lags endTime by up to ~2 s plus processing. Betting is accepted on status only, so close the betting UI locally at endTime.
  • The next round opens at the same moment the previous one closes (THAI-HILO then sends round_started), so the next round is usually open before the previous one is settled. For a short time Join Round may answer either round.
  • When settlement finishes, round_settled is sent (only if at least one player bet) and the round is then marked settled. A …/result call made the instant the event arrives can still show rolling; retry briefly.
  • If settlement fails, stakes are refunded, the round becomes cancelled and a replacement round opens. No event is sent for this.
  • Join Round endTime is RFC3339 UTC with whole seconds (e.g. 2026-09-30T11:45:30Z). SSE round_started endTime is RFC3339 with nanoseconds in the server's local time zone offset (e.g. 2026-09-30T18:45:30.123456789+07:00, or …Z). Parse both as dates; never compare them as strings.
  • No response or event carries the server time; the countdown depends on the client clock.
  • The rolling phase has no end timestamp: wait for round_settled (and, for THAI-HILO, round_started), or call Join Round again.

Reconnecting

  • The server sends no retry: and no id:, so the browser uses its default reconnect delay (about 3 s) and sends no useful Last-Event-ID.
  • Delivery is at most once: events published while the client is disconnected are lost. Each connection buffers up to 64 events; beyond that events are dropped without notice.
  • A non-200 answer at (re)connect (401 expired session, 429 rate limit, 503 maintenance, 404 bad gameCode, 400 missing currency) closes the EventSource for good; the browser does not retry.
  • On onopen: call Join Round (GET …/round), and GET …/result for any round the player bet in and has not yet seen settled.
  • On onerror with readyState === CLOSED: refresh the session or back off, then create a new EventSource.

Events

EventGamesWhen
round_startedTHAI-HILOThe server opened a new betting round for this room + currency + demo/real: at start-up, at the moment the previous round closes, in the periodic recovery sweep, as a replacement after a cancelled round, or when recovering a stuck round.
player_joinedTHAI-HILOAny player's Join Round call (GET …/rooms/{roomId}/round) finds a betting or rolling round: every call, repeats included. Not sent on SSE connect.
bet_placedTHAI-HILOA THAI-HILO room bet (POST …/rooms/{roomId}/bet) has been fully accepted: debited, stored, round still betting. One event per bet call.
seed_slot_updatedTHAI-HILO, LOTTOA bet in a MULTI_CSEED round (POST …/rooms/{roomId}/bet, or LOTTO tickets/create) claimed a client-seed slot. Sent right after the debit and the claim, before the bet is finally stored: if the bet then fails (500, or 99942 with the stake refunded), the event has already gone out and the slot stays claimed.
round_settledTHAI-HILO, LOTTOEnd of settlement, ONLY when at least one player bet in the round (rounds with no bets settle silently). Sent before the round is marked settled.

round_started

The server opened a new betting round for this room + currency + demo/real: at start-up, at the moment the previous round closes, in the periodic recovery sweep, as a replacement after a cancelled round, or when recovering a stuck round.

FieldTypeDescription
roundIDstringNew round id. Key is roundID (capital ID).
endTimestringWhen betting closes: RFC3339 with nanoseconds in the server's local time zone offset. Parse as a date.
seedHashstringSHA-256 hex server-seed commitment.
{
    "type": "round_started",
    "data": {
        "roundID": "tbq1x0a7c3e91f2",
        "endTime": "2026-09-30T18:45:30.123456789+07:00",
        "seedHash": "9f2c1e7a5b3d8f40c6a1e2d9b7f35c08e4a6d1b9c2f7e3a05d8b6c4e1f2a9d37"
    }
}
  • LOTTO never sends it. LOTTO clients learn about a new round only by calling Join Round or POST /gapi/v1/g/lotto/round (e.g. after round_settled or once endTime passes).
  • MULTI_CSEED rounds start with default seed slots, but the event does not carry clientSeedSlots; call Join Round to get them.

player_joined

Any player's Join Round call (GET …/rooms/{roomId}/round) finds a betting or rolling round: every call, repeats included. Not sent on SSE connect.

FieldTypeDescription
bettorCountintegerNumber of distinct players who have placed a bet in the current round (not viewers or joiners); 0 when nobody has bet yet.
{
    "type": "player_joined",
    "data": {
        "bettorCount": 3
    }
}
  • Carries no roundID and no player identity.
  • Because every Join Round call triggers it, a client polling Join Round floods the room with this event.

bet_placed

A THAI-HILO room bet (POST …/rooms/{roomId}/bet) has been fully accepted: debited, stored, round still betting. One event per bet call.

FieldTypeDescription
usernamestringBettor's username for real players; "" for demo players.
areastringAreas of that call joined with "," (duplicates kept).
amountstringTotal stake of that call, not per area. Decimal string, e.g. "150".
bettorCountintegerDistinct bettors in the round after this bet.
{
    "type": "bet_placed",
    "data": {
        "username": "player_demo01",
        "area": "HI,1-2",
        "amount": "150",
        "bettorCount": 3
    }
}
  • LOTTO never sends it (bet privacy).
  • No roundID and no per-area amounts.
  • The bettor also receives their own event, and real players' usernames reach every subscriber of the room.

seed_slot_updated

A bet in a MULTI_CSEED round (POST …/rooms/{roomId}/bet, or LOTTO tickets/create) claimed a client-seed slot. Sent right after the debit and the claim, before the bet is finally stored: if the bet then fails (500, or 99942 with the stake refunded), the event has already gone out and the slot stays claimed.

FieldTypeDescription
indexinteger0-based slot index.
seedstringClaimer's client seed.
nonceintegerClaimer's nonce at claim time.
ownerPlayerIDintegerClaimer's player id. Key is ownerPlayerID (capital ID).
{
    "type": "seed_slot_updated",
    "data": {
        "index": 0,
        "seed": "player-seed-01",
        "nonce": 4,
        "ownerPlayerID": 1000123
    }
}
  • Same keys as Join Round clientSeedSlots items: replace the local slot at index.
  • No roundID: apply it to the round currently shown.

round_settled

End of settlement, ONLY when at least one player bet in the round (rounds with no bets settle silently). Sent before the round is marked settled.

FieldTypeDescription
gameResultobjectTHAI-HILO: {dice, winAreas} (no total / isHI / isLO; derive them or call …/result). LOTTO: {drawnNumber, bottom5, bottom4, bottom3, bottom2, front2, runBot, runFront} (no luckyNumber).
playerResultobjectPresent only when this connection's own player bet in the round; other players' results are never sent.
roundIDstringSettled round. Key is roundID (capital ID).
seedRevealedstringRevealed seed: the 6-part MULTI seed string, or the plain round server seed (hex) for MULTI_CSEED.
{
    "type": "round_settled",
    "data": {
        "gameResult": {
            "dice": [
                1,
                2,
                6
            ],
            "winAreas": [
                "1",
                "1-2",
                "1-LO",
                "2",
                "2-LO",
                "6",
                "6-1",
                "6-2",
                "6-LO",
                "LO"
            ]
        },
        "playerResult": {
            "totalBet": "150",
            "totalWin": "96",
            "netWin": "-54",
            "balanceAfter": "9946"
        },
        "roundID": "tbq1x0a7c3e91f2",
        "seedRevealed": "default_THAI-HILO_1_1790768730123456789_126_4f1a9c2e"
    }
}
  • For THAI-HILO this is the reliable result source: GET …/result answers status: "betting" for about 5 minutes after the round closes.
  • A GET …/result call made right after this event may still say rolling; retry briefly.
  • playerResult is filtered per connection to the caller's own player.
  • Data keys arrive in alphabetical order.

Not sent

  • No round_cancelled, round_closed or round_rolling: nothing is sent when betting closes or a round is cancelled. Close the betting UI locally at endTime.
  • No round_started for LOTTO.
  • No player_joined or bet_placed for LOTTO.
  • No round_settled for rounds with zero bets.
  • No heartbeat, ping or keep-alive frame of any kind.
Modified at 2026-09-30 13:23:45
Previous
Request and Response
Next
Lotto Rules — หวยเฮงเฮง
Built with