Rounds, Transactions & Reports
APIs for querying game records and money flows. Use them for player bet history screens, admin round lookups, wallet ledger reconciliation, and daily GGR aggregation.
| Method | Path | Purpose |
|---|---|---|
| GET | /operator/v1/rounds | Round list per player |
| GET | /operator/v1/rounds/{round_id} | Round details (player view / full round) |
| GET | /operator/v1/sessions/{session_id} | Details of one seating at a buy-in flow table (table session) |
| GET | /operator/v1/transactions | Wallet transaction ledger (for reconciliation) |
| GET | /operator/v1/reports/ggr | GGR aggregated by day and game |
Sign every request according to the Authentication & signing rules. GET requests have no body and are signed with an empty string.
Common query parameters
List APIs read the period and page size parameters the same way.
| Parameter | Format | Description |
|---|---|---|
from | RFC 3339 (2026-10-18T00:00:00Z) or YYYY-MM-DD | Start of the range (inclusive). If omitted, 24 hours before to |
to | RFC 3339 or YYYY-MM-DD | End of the range (exclusive). If omitted, now (UTC) |
limit | integer | Rows per page. Default 100, max 500 (values above 500 become 500) |
YYYY-MM-DDmeans 00:00 UTC on that date. To query a day in another time zone, send RFC 3339 with an offset (e.g.2026-10-18T00:00:00+08:00).- A malformed
fromortois replaced with the default (last 24 hours) without an error. If results are not what you expect, check the parameter encoding first. - Percent-encode query values and sign the exact string you send. In particular, the
+in+08:00must be encoded as%2B. If it is not encoded,+is read as a space, date parsing fails, and the default is applied. Sending UTC (Z) avoids this problem.
Practice games are not recorded
Hands where no real money changes hands, such as practice games where a player sits alone against the computer, are not recorded in round or transaction records.
GET /operator/v1/rounds
Returns the rounds in which this operator's players took part, one row per player × round. If three of your players were in one round, there are three rows. Only rounds that have been settled (settled) or voided (void) are listed; rounds in progress appear after they finish.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
from, to | time | No | Range [from, to) based on the round start time. Even with sort=settled_at, the range filters by start time |
external_id | string | No | Only this player's rounds |
game_code | string | No | Only rounds of this game (Game codes) |
sort | started_at | settled_at | No | Sort key. Default started_at |
order | desc | asc | No | Sort direction. Default desc (newest first) |
limit | integer | No | Default 100, max 500 |
cursor | string | No | next_cursor from the previous response |
Request example
GET /operator/v1/rounds?external_id=user_1001&from=2026-10-18T00:00:00Z&to=2026-10-19T00:00:00Z&limit=100 HTTP/1.1
Host: {API_HOST}
X-Operator: acme
X-Api-Key: ak_live_…
X-Timestamp: 1760839200123
X-Nonce: 9f2c4e1a7b3d5e6f8a9b0c1d
X-Signature: 3b6f…Response example
{
"rounds": [
{
"round_id": "rnd_0199f3a2c4e57b1d9a2e4f6081c3d5e7",
"game_code": "live_holdem",
"table_id": "tbl_0198c1d2e3f47a5b8c9d0e1f2a3b4c5d",
"started_at": "2026-10-18T14:10:00.123456Z",
"settled_at": "2026-10-18T14:11:03.5Z",
"status": "settled",
"external_id": "user_1001",
"bet": 12000,
"win": 30400,
"rake": 600,
"currency": "PHP",
"hand_no": 66,
"table_name": "Live Hold'em 1",
"table_names": { "en": "Live Hold'em 1", "ko": "라이브 홀덤 1" }
}
],
"next_cursor": "AZnzosTlex2aLk9ggcPV5wGZ86LE5XsdmiwvYIHD1ec"
}Response fields
| Field | Type | Description |
|---|---|---|
round_id | string | Round ID (rnd_…) |
game_code | string | Game code |
table_id | string | Table ID (tbl_…) |
started_at, settled_at | time | Start time / settlement (or void) time |
status | string | settled (settled) or void (voided — a round whose bets were returned) |
external_id | string | Player |
bet | int64 | Amount paid in this round (sum of bets, blinds, antes, calls, etc.) |
win | int64 | Amount received in this round (after rake). 0 for a lost round |
rake | int64 | Rake / commission taken from this player's share |
currency | string | Table currency |
hand_no | int64 | Round number within the table |
table_name, table_names | string, object | Table name / names by language |
next_cursor | string | null | Cursor for the next page. null on the last page |
- Profit/loss is
win − bet. Buy-in flow games (Hold'em, etc.) do not call the wallet every round, so this value is profit/loss in chips; wallet transactions occur separately per table session (buy_in,top_up,cash_out). → GET /sessions/{session_id} - Player rows of the same round are ordered by round ID, then by player.
Pagination
Pass next_cursor as cursor together with the same sort, order, and filters. The cursor is an opaque string; do not parse or construct it. If a page has fewer rows than limit, next_cursor is null (if the row count is exactly limit, the next call may return an empty list).
When sorting with sort=settled_at, rows without a settlement time are treated as the latest.
Errors
| HTTP | Code | Situation |
|---|---|---|
| 500 | INTERNAL_ERROR | Temporary server error — retry after a short while |
Authentication, signature, and rate limit errors are common to all endpoints. → Error codes
GET /operator/v1/rounds/{round_id}
Details of a single round. There are two views depending on whether you pass external_id.
| View | Request | Purpose | Contents |
|---|---|---|---|
| Player view | ?external_id=user_1001 | Bet details the site shows to the member themselves | That member's hole cards (cards.player), bet summary (bets), that member's share of the rake, that member's wallet transactions |
| Full round | No external_id | Operator admin lookups and dispute checks | Round money (money), progress log (history), card flow (hands), and dealer card input (card_log) for your own table in independent mode |
Both views include the round money (money).
Path and query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
round_id | string (path) | Yes | Round ID (rnd_…) |
external_id | string (query) | No | The player to view the round as. If that player was not in this round, ROUND_NOT_FOUND |
Visibility
- A round in which none of this operator's players took part returns
404 ROUND_NOT_FOUND. A round in progress returns the same error because there is no participant settlement record yet.- Exception: rounds on an independent mode operator's own dedicated table can be opened in the full round view (without
external_id) even with no participants (to check dealer card input). In this casemoneyis an empty shape.
- Exception: rounds on an independent mode operator's own dedicated table can be opened in the full round view (without
- Players of other operators sit at shared tables too. For those players, only the seat and amounts are visible; their identity (
external_id, nickname) and unrevealed hole cards are not. - Wallet transactions and bet items include only this operator's players.
Do not pass the player view response to members as-is
The player view (external_id) response also includes money in its full round shape, which contains other members' external_id, nicknames, and amounts at the same round, as well as those members' wallet transactions (money.transactions). On the member screen, show only the member's own data such as cards, bets, rake, and transactions.
Request example
GET /operator/v1/rounds/rnd_0199f3a2c4e57b1d9a2e4f6081c3d5e7 HTTP/1.1
Host: {API_HOST}
X-Operator: acme
X-Api-Key: ak_live_…
X-Timestamp: 1760839200123
X-Nonce: 1c7d0e5b9a2f4c6e8d1b3a5f
X-Signature: 8e1a…Response — top-level fields
| Field | Type | Description |
|---|---|---|
round_id, game_code, table_id, hand_no | Round identification | |
status | string | settled or void (rounds on your own table in independent mode can also show open while in progress) |
started_at, settled_at | time | Start / settlement time |
currency | string | Table currency |
result | object | null | Raw outcome from the game engine. The shape differs per game (see result below) |
video_url | null | Reserved field. Currently always null |
money | object | Round money — who paid and received how much (money) |
cards | object | { "player": [...] } — in the player view, that member's final cards; otherwise null |
bets | array | In the player view, a one-line summary [{ "spot": "hand", "amount": amount paid, "win": amount received }]; otherwise [] |
rake | int64 | In the player view, that member's share; otherwise the rake for the whole round (money.totals.rake) |
transactions | array | In the player view, that member's wallet transactions; otherwise the same as money.transactions |
history | array | Full round view only. Progress log (history) |
hands | array | Full round view only. Card flow per player of this operator (hands) |
card_log | array | Full round view + rounds on your own table in independent mode only. Dealer card input records (same shape as entries of GET /dealer-card-log) |
bets in the player view is a one-line summary regardless of the game. Baccarat per-spot bets and Blackjack per-box bets are in money.bets.
money — round money
| Field | Type | Description |
|---|---|---|
kind | string | p2p (player vs. player — Texas Hold'em, Pot-Limit Omaha, Live Hold'em, Seotda, Badugi, Matgo, Go-Stop, Tong-its) or banked (against the house — Speed Hold'em, Blackjack, Baccarat, Red Dog) |
currency | string | Currency |
players[] | array | Totals per participant (table below) |
pots[] | array | Money movement per pot. Hold'em family, Seotda, and Badugi only, and only for rounds with records (omitted otherwise) |
returns[] | array | Uncalled excess bets returned [{seat, external_id, amount}] (omitted if none) |
rake_rule | object | Rake rule applied to the round (Hold'em family; omitted if none) |
rake_on_profit | bool | If true, the dealer fee is taken from the winner's net profit rather than the pot (Seotda, Badugi). In this case the pot shares are amounts before the dealer fee, and the actual dealer fee is the per-seat rake |
bets[] | array | Bet items of this operator's players (banked) |
transactions[] | array | This operator's players' wallet transactions tied to this round (banked. For buy-in flow games, wallet transactions are tied to the table session, so this is usually an empty array) |
totals | object | Totals |
players[]
| Field | Type | Description |
|---|---|---|
external_id | string | null | The value if the player belongs to this operator; null for another operator's player |
nickname | string | Only for this operator's players (omitted otherwise) |
seat | int | Seat number. -1 for games without seats (Baccarat, etc.) |
debit | int64 | Amount paid in this round |
credit | int64 | Amount received in this round (after rake) |
rake | int64 | This player's share of rake / commission |
net | int64 | credit − debit |
stack_before, stack_after | int64 | Seat chips before / after the round (only for games played with seat chips — P2P, Red Dog) |
mine | bool | Whether this is this operator's player |
bets[] — { external_id, spot, amount, win, status }. spot differs per game: for Baccarat player, banker, tie, player_pair, banker_pair, either_pair, perfect_pair, player_bonus, banker_bonus; for Speed Hold'em and Blackjack box_N (Blackjack additional decisions are box_N:extra). win is the amount received (including the stake), and status is settled or rolled_back.
transactions[] — { tx_id, external_id, type, reason, amount, status, balance_after, created_at, completed_at, ref_tx_id }. Each field has the same meaning as in GET /transactions.
totals — { debit, credit, rake, house, balanced }
| Field | Description |
|---|---|
debit, credit, rake | Sum over all participants (including other operators) |
house | House profit/loss = debit − credit. For P2P, equal to the rake |
balanced | For P2P, true if debit = credit + rake (no chips leaked). Always true for banked |
pots[] (Hold'em family example)
| Field | Description |
|---|---|
index | 0 = main pot, 1+ = side pots |
amount | Pot total |
rake, rake_full, rake_cum | Rake actually taken / rake that would have been taken at the full rate (before the cap) / cumulative from the main pot up to this pot |
capped | Whether less was taken because the per-round maximum fee (cap) was reached |
floored | Amount not taken due to rounding down fractions (only when present) |
eligible | Seats eligible to win this pot |
contributions[], shares[] | Seats and amounts that put in / received {seat, external_id, amount} (external_id: null for other operators' players) |
rank | Winning hand rank (only when present) |
balanced | Sum put in = pot total = sum received + rake |
rake_rule — { percent, cap, no_flop_no_drop, floor?, skip? }. percent is in bp (500 = 5%), cap is the per-round maximum fee (0 = none), and skip is the reason no rake was taken (off, no_flop).
history — progress log
A chronological list of how the round progressed. Each item has ts and kind; the remaining fields are filled depending on kind (fields without a value are omitted).
kind | Meaning | Main fields |
|---|---|---|
start | Round start | no (round number), seats[] ({seat, player, chips}), button, sb, bb, amount (ante) |
deal | Hole card deal | seat, player, cards |
card | A single card (live dealer input / automatic) | position, cards, stage, source (scanner, wedge, manual, virtual, auto), dealer, reason (rejection code) |
street | Next street | stage, board |
betting | Betting opened / closed | action (open, closed), stage, amount |
action | Player decision | seat, player, action (fold, check, call, bet, raise, allin, hit, stand, go, stop …), amount, total, pot, stack, auto (automatic decision on timeout) |
bet, bet_undo | Banked bet / bet cancellation | player, amount, items[] ({spot, amount, win, status}) |
showdown | Hand reveal | seat, player, cards, rank, best |
result | Outcome / distribution | pots[] ({index, amount, rake, rank, winners[]}), rake, board, outcome |
tx | Wallet transaction | player, action (debit, credit, rollback), reason, amount, status |
void | Void | reason |
settle | Final settlement (always the last line) | rows[] ({seat, player, invested, won, rake, net, stack_after}), amount, won, rake |
event | Other events | type, data |
playeris{ "external_id", "nickname" }; for another operator's player it is{ "other": true }.- A
nullelement incardsis a hidden card (another operator's player's unrevealed hole card). - The dealer name (
dealer) is filled only for rounds on your own table in independent mode. - The progress log is for display.
kindvalues and fields may be added, so implement it to skip unknown values. Base money calculations onmoney.
hands — card flow
For each of this operator's players, summarizes first cards received → cards exchanged/discarded during the round → cards after the round on one line. Sorted by seat; other operators' players are excluded.
| Field | Description |
|---|---|
seat, player | Seat / {external_id, nickname} |
dealt | First cards received (Hold'em family hole cards, 4 cards in Badugi, 2 cards in Seotda, opening hand in Tong-its / Matgo) |
steps[] | Hand changes during the round {stage?, out, in} — out cards discarded (played), in cards received. In Badugi, stage is the draw turn (draw_1 morning, draw_2 afternoon, draw_3 evening), and turns with no exchange remain as empty arrays |
held | Cards left in hand at the end of the round (only when different from final — Matgo, Go-Stop) |
final | Cards after the round (for Matgo and Go-Stop, the captured cards) |
board | Community cards (Hold'em family) |
best, rank | Best cards and hand rank when the hand was revealed |
Cards are written as 2-character playing card codes (AS, TD, 9H); hwatu cards are 2-digit month + letter (01a, 05b).
Example 1 — Texas Hold'em (P2P, full round view)
{
"round_id": "rnd_0199f3a2c4e57b1d9a2e4f6081c3d5e7",
"game_code": "holdem_nlh",
"table_id": "tbl_0198c1d2e3f47a5b8c9d0e1f2a3b4c5d",
"hand_no": 66,
"status": "settled",
"started_at": "2026-10-18T14:10:00.123456Z",
"settled_at": "2026-10-18T14:11:03.5Z",
"currency": "PHP",
"result": {
"board": ["2C", "3D", "4H", "9S", "TC"],
"awards": [{ "pot_index": 0, "seats": [3], "amount": 1520, "shares": [{ "seat": 3, "amount": 1520 }], "rake": 80, "rank": "two_pair", "best5": ["9S", "9D", "4H", "4C", "TC"] }],
"rake": 80,
"showdown": true,
"pots": [ "…" ],
"returns": [],
"rake_rule": { "percent": 500, "cap": 0, "no_flop_no_drop": true }
},
"video_url": null,
"money": {
"kind": "p2p",
"currency": "PHP",
"players": [
{ "external_id": null, "seat": 3, "debit": 800, "credit": 1520, "rake": 40, "net": 720, "stack_before": 20000, "stack_after": 20720, "mine": false },
{ "external_id": "user_1001", "nickname": "lucky7", "seat": 4, "debit": 800, "credit": 0, "rake": 40, "net": -800, "stack_before": 32000, "stack_after": 31200, "mine": true }
],
"pots": [
{
"index": 0, "amount": 1600, "rake": 80, "rake_full": 80, "rake_cum": 80, "capped": false,
"eligible": [3, 4],
"contributions": [ { "seat": 3, "external_id": null, "amount": 800 }, { "seat": 4, "external_id": "user_1001", "amount": 800 } ],
"shares": [ { "seat": 3, "external_id": null, "amount": 1520 } ],
"rank": "two_pair",
"balanced": true
}
],
"rake_rule": { "percent": 500, "cap": 0, "no_flop_no_drop": true },
"bets": [],
"transactions": [],
"totals": { "debit": 1600, "credit": 1520, "rake": 80, "house": 80, "balanced": true }
},
"cards": { "player": null },
"bets": [],
"rake": 80,
"transactions": [],
"history": [
{ "ts": "2026-10-18T14:10:00.2Z", "kind": "start", "type": "hand_start", "no": 66, "button": 3, "sb": 3, "bb": 4,
"seats": [ { "seat": 3, "player": { "other": true }, "chips": 20000 }, { "seat": 4, "player": { "external_id": "user_1001", "nickname": "lucky7" }, "chips": 32000 } ] },
{ "ts": "2026-10-18T14:10:00.3Z", "kind": "deal", "seat": 3, "player": { "other": true }, "cards": [null, null] },
{ "ts": "2026-10-18T14:10:00.3Z", "kind": "deal", "seat": 4, "player": { "external_id": "user_1001", "nickname": "lucky7" }, "cards": ["AS", "KD"] },
{ "ts": "2026-10-18T14:10:05.1Z", "kind": "action", "type": "action", "seat": 3, "player": { "other": true }, "action": "call", "amount": 200, "pot": 400, "stack": 19800 },
{ "ts": "2026-10-18T14:10:09.8Z", "kind": "street", "stage": "flop", "board": ["2C", "3D", "4H"] },
{ "ts": "2026-10-18T14:11:02.9Z", "kind": "showdown", "seat": 3, "player": { "other": true }, "cards": ["9D", "4C"], "rank": "two_pair", "best": ["9S", "9D", "4H", "4C", "TC"] },
{ "ts": "2026-10-18T14:11:03.5Z", "kind": "settle", "amount": 1600, "won": 1520, "rake": 80,
"rows": [ { "seat": 3, "player": { "other": true }, "invested": 800, "won": 1520, "rake": 40, "net": 720, "stack_after": 20720 },
{ "seat": 4, "player": { "external_id": "user_1001", "nickname": "lucky7" }, "invested": 800, "won": 0, "rake": 40, "net": -800, "stack_after": 31200 } ] }
],
"hands": [
{ "seat": 4, "player": { "external_id": "user_1001", "nickname": "lucky7" },
"dealt": ["AS", "KD"], "final": ["AS", "KD"], "board": ["2C", "3D", "4H", "9S", "TC"], "best": ["AS", "KD", "TC", "9S", "4H"], "rank": "high_card" }
]
}Querying the same round with ?external_id=user_1001 drops history and hands and changes the following values.
{
"cards": { "player": ["AS", "KD"] },
"bets": [ { "spot": "hand", "amount": 800, "win": 0 } ],
"rake": 40,
"transactions": []
}Example 2 — Live Baccarat (banked)
In banked games the wallet moves every round, so money.bets holds the per-spot bets and money.transactions holds that round's debits and credits. There are no pots.
{
"round_id": "rnd_0199f3b0aa127c3e8f45d6a7b8c9d0e1",
"game_code": "live_baccarat",
"status": "settled",
"currency": "PHP",
"result": {
"winner": "banker",
"player": { "cards": ["9H", "7C", "KD"], "total": 6, "natural": false, "pair": false, "perfect": false },
"banker": { "cards": ["8S", "QH"], "total": 8, "natural": true, "pair": false, "perfect": false },
"shoe_no": 12, "hand_no": 31,
"winning_spots": ["banker"], "push_spots": [],
"total_bet": 150000, "total_win": 97500, "players": 14,
"winners": [ { "name": "lucky7", "amount": 9500 } ]
},
"money": {
"kind": "banked",
"currency": "PHP",
"players": [
{ "external_id": "user_1001", "nickname": "lucky7", "seat": -1, "debit": 6000, "credit": 9500, "rake": 0, "net": 3500, "mine": true }
],
"bets": [
{ "external_id": "user_1001", "spot": "banker", "amount": 5000, "win": 9500, "status": "settled" },
{ "external_id": "user_1001", "spot": "tie", "amount": 1000, "win": 0, "status": "settled" }
],
"transactions": [
{ "tx_id": "tx_0199f3b0ab3c7d118e2f3a4b5c6d7e8f", "external_id": "user_1001", "type": "debit", "reason": "bet", "amount": 6000,
"status": "ok", "balance_after": 1244000, "created_at": "2026-10-18T14:20:11.402Z", "completed_at": "2026-10-18T14:20:11.455Z", "ref_tx_id": null },
{ "tx_id": "tx_5c0e9a7d3b1f5e2a8c4d6b0f9e1a3c5d", "external_id": "user_1001", "type": "credit", "reason": "settle", "amount": 9500,
"status": "ok", "balance_after": 1253500, "created_at": "2026-10-18T14:20:41.020Z", "completed_at": "2026-10-18T14:20:41.071Z", "ref_tx_id": null }
],
"totals": { "debit": 6000, "credit": 9500, "rake": 0, "house": -3500, "balanced": true }
}
}(Some fields are omitted from the example above.)
Example 3 — hands by game
Badugi — the three draws (steps) are kept together with the turn names.
{ "seat": 2, "player": { "external_id": "user_1001", "nickname": "lucky7" },
"dealt": ["AS", "2H", "KS", "QS"],
"steps": [
{ "stage": "draw_1", "out": ["KS", "QS"], "in": ["3D", "QS"] },
{ "stage": "draw_2", "out": [], "in": [] },
{ "stage": "draw_3", "out": ["QS"], "in": ["4C"] }
],
"final": ["AS", "2H", "3D", "4C"], "rank": "badugi" }Matgo / Go-Stop — steps are the cards played, final the captured cards, and held the cards left in hand at the end of the round.
{ "seat": 0, "player": { "external_id": "user_1001", "nickname": "lucky7" },
"dealt": ["01a", "05b", "07c"],
"steps": [ { "out": ["01a"], "in": [] } ],
"held": ["05b", "07c"],
"final": ["01a", "01b"] }result — raw game outcome
result is the raw outcome the game engine records when it finishes a round, so its shape differs per game and fields may be added without notice. Use it as a reference when displaying the outcome on screen, but use money for amount calculations and reconciliation.
| Game | Main fields |
|---|---|
Hold'em family (holdem_nlh, holdem_plo4, live_holdem) | board, awards[] ({pot_index, seats, amount, shares, rake, rank, best5}), rake, showdown, pots, returns, rake_rule |
Baccarat (live_baccarat) | winner (player, banker, tie), player, banker ({cards, total, natural, pair, perfect}), shoe_no, hand_no, winning_spots, push_spots, total_bet, total_win, players, winners[] ({name, amount}) |
| Voided round (all games) | { "void_reason": "…" } |
Errors
| HTTP | Code | Situation |
|---|---|---|
| 404 | ROUND_NOT_FOUND | Malformed ID, nonexistent round, a round without any of this operator's players, a round in progress, or the external_id player was not in this round |
| 500 | INTERNAL_ERROR | Temporary server error |
GET /operator/v1/sessions/{session_id}
Details of a table session (from sitting down once until standing up) in buy-in flow games (Texas Hold'em, Pot-Limit Omaha, Live Hold'em, Seotda, Matgo, Tong-its, Red Dog, etc.). These games do not call the wallet every round; they create wallet transactions only when the player sits down (buy_in), tops up (top_up), or leaves the seat (cash_out), and the round_id of those transactions is the table session ID (tss_…). This API aggregates the per-round profit/loss during the session and shows whether the buy-in and cash-out match the per-round profit/loss.
Get the session_id from the round_id of GET /transactions (a value starting with tss_), or from the round_id in the wallet callback body.
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
session_id | string | Yes | Table session ID (tss_…) |
Request example
curl {API_BASE}/operator/v1/sessions/tss_0199f2e1d0c3b4a5968778695a4b3c2d \
-H 'X-Operator: acme' -H 'X-Api-Key: ak_live_…' \
-H 'X-Timestamp: 1760839200123' -H 'X-Nonce: 5e8f…' -H 'X-Signature: 0d4c…'Response example
{
"session_id": "tss_0199f2e1d0c3b4a5968778695a4b3c2d",
"external_id": "user_1001",
"nickname": "lucky7",
"table_id": "tbl_0198c1d2e3f47a5b8c9d0e1f2a3b4c5d",
"table_name": "Hold'em 2/4",
"table_names": { "en": "Hold'em 2/4" },
"game_code": "holdem_nlh",
"currency": "PHP",
"status": "closed",
"opened_at": "2026-10-18T13:02:11.08Z",
"closed_at": "2026-10-18T14:30:45.91Z",
"buy_in_total": 960000,
"cash_out": 302582,
"chips": 0,
"rounds": [
{ "round_id": "rnd_0199f3a2c4e57b1d9a2e4f6081c3d5e7", "hand_no": 68, "status": "settled",
"started_at": "2026-10-18T13:05:00.1Z", "settled_at": "2026-10-18T13:06:12.4Z",
"seat": 3, "debit": 324000, "credit": 944000, "rake": 8000, "net": 620000, "stack_after": 944000 }
],
"transactions": [
{ "tx_id": "tx_7a1b2c3d4e5f60718293a4b5c6d7e8f9", "external_id": "user_1001", "type": "debit", "reason": "buy_in", "amount": 480000,
"status": "ok", "balance_after": 1520000, "created_at": "2026-10-18T13:02:11.1Z", "completed_at": "2026-10-18T13:02:11.2Z", "ref_tx_id": null },
{ "tx_id": "tx_1f2e3d4c5b6a79880f1e2d3c4b5a6978", "external_id": "user_1001", "type": "credit", "reason": "cash_out", "amount": 302582,
"status": "ok", "balance_after": 1342582, "created_at": "2026-10-18T14:30:45.9Z", "completed_at": "2026-10-18T14:30:46.0Z", "ref_tx_id": null }
],
"totals": { "rounds": 15, "debit": 1997529, "credit": 1340111, "rake": 58319, "net": -657418 },
"reconcile": { "paid_in": 960000, "paid_out": 302582, "unsettled": 0, "expected_end": 302582, "actual_end": 302582, "balanced": true, "closed": true }
}Response fields
| Field | Type | Description |
|---|---|---|
session_id, external_id, nickname | Session / player | |
table_id, table_name, table_names, game_code, currency | Table information | |
status | string | opening (buy-in in progress), seated (seated), kept (Live Hold'em — the player has left the seat with chips held), closing (leaving the seat in progress), closed (finished) |
opened_at, closed_at | time | Sit-down / leave time (closed_at is null until finished) |
buy_in_total | int64 | Total of requested buy-ins and top-ups. Includes requested amounts whose wallet debit failed, so use reconcile.paid_in for reconciliation |
cash_out | int64 | null | Cash-out amount (null until finished) |
chips | int64 | Current seat chips (usually 0 for a finished session) |
rounds[] | array | Rounds played during the session (in start order, up to 5,000 rounds): round_id, hand_no, status, started_at, settled_at, seat, debit, credit, rake, net, stack_after |
transactions[] | array | Wallet transactions of this session (in creation order) |
totals | object | Sums of rounds (number of rounds), debit, credit, rake, net |
reconcile | object | Session reconciliation result (below) |
reconcile
| Field | Description |
|---|---|
paid_in | Money actually received from the wallet = successful debits (buy-in, top-up) − successful rollbacks of those debits |
paid_out | Money paid out to the wallet = sum of cash-out credits (not failed) |
unsettled | Number of credits and rollbacks not yet finished (pending, uncertain) |
expected_end | paid_in + Σcredit − Σdebit — the ending amount calculated from per-round profit/loss |
actual_end | paid_out for a finished session; current chips for an open session |
balanced | true if expected_end = actual_end and unsettled = 0 |
closed | Whether the session has finished through cash-out |
balanced: false together with unsettled > 0 means the cash-out credit is still being retried. Query again after a short while.
Errors
| HTTP | Code | Situation |
|---|---|---|
| 404 | SESSION_NOT_FOUND | Malformed ID, nonexistent session, or another operator's session |
| 500 | INTERNAL_ERROR | Temporary server error |
GET /operator/v1/transactions
The ledger of debit, credit, and rollback transactions the platform sent to this operator's wallet (balance queries are not recorded). Use it for reconciliation, matching against the operator wallet ledger by tx_id.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
from, to | time | No | [from, to) based on the time the platform created the transaction (created_at). Default: last 24 hours |
status | ok | failed | pending | uncertain | No | Only this status |
type | debit | credit | rollback | No | Only this type |
external_id | string | No | Only this player |
limit | integer | No | Default 100, max 500 |
cursor | string | No | next_cursor from the previous response |
Request example
GET /operator/v1/transactions?from=2026-10-18T00:00:00Z&to=2026-10-19T00:00:00Z&limit=500 HTTP/1.1
Host: {API_HOST}
X-Operator: acme
X-Api-Key: ak_live_…
X-Timestamp: 1760839200123
X-Nonce: 2b4d6f8a0c1e3a5b7d9f1a3c
X-Signature: c71e…Response example
{
"transactions": [
{
"tx_id": "tx_0199f3b0ab3c7d118e2f3a4b5c6d7e8f",
"type": "debit",
"ref_tx_id": null,
"external_id": "user_1001",
"amount": 6000,
"currency": "PHP",
"reason": "bet",
"round_id": "rnd_0199f3b0aa127c3e8f45d6a7b8c9d0e1",
"game_code": "live_baccarat",
"table_id": "tbl_0198d2e3f4a57b6c9d0e1f2a3b4c5d6e",
"status": "ok",
"operator_tx_id": "w-1a2b3c4d5e6f",
"balance_after": 1244000,
"created_at": "2026-10-18T14:20:11.402Z",
"completed_at": "2026-10-18T14:20:11.455Z"
},
{
"tx_id": "tx_7a1b2c3d4e5f60718293a4b5c6d7e8f9",
"type": "debit",
"ref_tx_id": null,
"external_id": "user_2002",
"amount": 480000,
"currency": "PHP",
"reason": "buy_in",
"round_id": "tss_0199f2e1d0c3b4a5968778695a4b3c2d",
"game_code": "holdem_nlh",
"table_id": "tbl_0198c1d2e3f47a5b8c9d0e1f2a3b4c5d",
"status": "ok",
"operator_tx_id": "w-9f8e7d6c5b4a",
"balance_after": 1520000,
"created_at": "2026-10-18T13:02:11.1Z",
"completed_at": "2026-10-18T13:02:11.2Z"
}
],
"next_cursor": "tx_7a1b2c3d4e5f60718293a4b5c6d7e8f9"
}Response fields
| Field | Type | Description |
|---|---|---|
tx_id | string | Transaction ID (tx_…). Same as the tx_id in the wallet callback body |
type | string | debit, credit, rollback |
ref_tx_id | string | null | For a rollback, the tx_id of the debit it cancels. Otherwise null |
external_id | string | Player |
amount | int64 | Amount (in the currency's minor unit, 0 or more) |
currency | string | Currency |
reason | string | Reason (buy_in, top_up, cash_out, bet, ante, double, split, settle, rollback reasons, etc.). For the list of values, see debit, credit, and rollback |
round_id | string | null | As sent in the wallet request — the round (rnd_…) for the bet flow, the table session (tss_…) for the buy-in flow |
game_code, table_id | string | null | As sent in the wallet request |
status | string | See the table below |
operator_tx_id | string | null | The operator_tx_id the operator returned in the wallet response |
balance_after | int64 | null | balance from the wallet response (null if it was not in the response) |
created_at | time | When the platform created the transaction (just before the first attempt) |
completed_at | time | null | When it finished (became ok, failed, or uncertain). null while in progress |
next_cursor | string | null | Cursor for the next page. null on the last page |
status
| Value | Meaning | Expected state in the operator wallet |
|---|---|---|
ok | The wallet responded with OK | Applied |
failed | The debit was rejected — the wallet returned a status other than OK, or in prepaid mode the prepaid credit was insufficient and the debit was failed without calling the wallet | Not applied (the call may not have been made at all) |
uncertain | No response was received for the debit (timeout, connection error, UNKNOWN_ERROR). The platform immediately sends a rollback with this debit as ref_tx_id | Debit and its rollback combined net to zero |
pending | In progress, or a credit/rollback being retried until it receives OK | Not final yet — check again later |
- Credits and rollbacks are retried until they receive
OK, so their final status isok. If a credit or rollback stayspendingfor a long time, check the wallet-side responses. → Wallet callback overview - A debit that stays
pendingfor too long is cleaned up by the platform sending a rollback.
Sorting and pagination
- Results are in descending
tx_idorder. New transaction IDs increase over time, but IDs derived deterministically from other transactions, such as settlement credits and rollbacks, are not time-ordered, so this is not strictly newest-first. If you need chronological order, sort bycreated_atafter receiving the data. next_cursoris thetx_idof the last row on that page. Passing it ascursorwith the same filters and the samefromandtocontinues from smallertx_idvalues. With a fixed range, there are no gaps or duplicates between pages.- If there are fewer rows than
limit,next_cursorisnull(if there are exactlylimitrows, the next call may return an empty list). - If
tois omitted, "now" changes on every call. When fetching everything, fixfromandto.
Fetching everything — example code
import { createHmac, randomBytes } from 'node:crypto';
const ORIGIN = '{API_BASE}'; // Per-environment address (provided during onboarding)
const OPERATOR = process.env.OPERATOR_CODE!; // e.g. acme
const API_KEY = process.env.OPERATOR_API_KEY!;
const API_SECRET = process.env.OPERATOR_API_SECRET!;
// Signed GET — sign the path exactly as encoded, including the query, and send it as-is
async function signedGet<T>(path: string): Promise<T> {
const ts = String(Date.now());
const nonce = randomBytes(12).toString('hex');
const sig = createHmac('sha256', API_SECRET)
.update(`${ts}\n${nonce}\nGET\n${path}\n`) // GET has no body (empty string)
.digest('hex');
const res = await fetch(ORIGIN + path, {
headers: { 'X-Operator': OPERATOR, 'X-Api-Key': API_KEY, 'X-Timestamp': ts, 'X-Nonce': nonce, 'X-Signature': sig },
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
return res.json() as Promise<T>;
}
type Tx = { tx_id: string; type: string; ref_tx_id: string | null; external_id: string; amount: number; status: string; created_at: string };
export async function fetchAllTransactions(from: string, to: string): Promise<Tx[]> {
const all: Tx[] = [];
let cursor: string | null = null;
do {
const q = new URLSearchParams({ from, to, limit: '500' }); // ':' is encoded as %3A
if (cursor) q.set('cursor', cursor);
const page = await signedGet<{ transactions: Tx[]; next_cursor: string | null }>(`/operator/v1/transactions?${q}`);
all.push(...page.transactions);
cursor = page.next_cursor;
} while (cursor);
return all;
}
// e.g. all of yesterday (UTC). Fix `to` sufficiently in the past
const rows = await fetchAllTransactions('2026-10-18T00:00:00Z', '2026-10-19T00:00:00Z');import hashlib, hmac, os, secrets, time
from urllib.parse import urlencode
import requests
ORIGIN = "{API_BASE}"
OPERATOR = os.environ["OPERATOR_CODE"]
API_KEY = os.environ["OPERATOR_API_KEY"]
API_SECRET = os.environ["OPERATOR_API_SECRET"]
def signed_get(path: str) -> dict:
"""path is the string encoded including the query — send it exactly as signed."""
ts = str(int(time.time() * 1000))
nonce = secrets.token_hex(12)
msg = f"{ts}\n{nonce}\nGET\n{path}\n".encode() # GET has no body
sig = hmac.new(API_SECRET.encode(), msg, hashlib.sha256).hexdigest()
r = requests.get(ORIGIN + path, timeout=10, headers={
"X-Operator": OPERATOR, "X-Api-Key": API_KEY,
"X-Timestamp": ts, "X-Nonce": nonce, "X-Signature": sig,
})
r.raise_for_status()
return r.json()
def fetch_all_transactions(frm: str, to: str) -> list[dict]:
rows, cursor = [], None
while True:
params = {"from": frm, "to": to, "limit": 500}
if cursor:
params["cursor"] = cursor
page = signed_get("/operator/v1/transactions?" + urlencode(params))
rows.extend(page["transactions"])
cursor = page["next_cursor"]
if not cursor:
return rows
rows = fetch_all_transactions("2026-10-18T00:00:00Z", "2026-10-19T00:00:00Z")Calls are limited to 200 per second per operator. When fetching in bulk, use limit=500 to reduce the number of calls.
Errors
| HTTP | Code | Situation |
|---|---|---|
| 500 | INTERNAL_ERROR | Temporary server error |
A malformed cursor is ignored without an error, and the first page is returned.
GET /operator/v1/reports/ggr
GGR (gross gaming revenue) aggregated by date × game over settled rounds.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
from, to | time | No | [from, to) based on the round's settlement time. Default: last 24 hours |
game_code | string | No | Only this game |
external_id | string | No | Only this player's bets, wins, and rake. For a nonexistent player, an empty result { "currency": "", "rows": [] } |
Other parameters (grouping key, time zone, etc.) are not supported and are ignored if sent.
Request example
GET /operator/v1/reports/ggr?from=2026-10-01T00:00:00Z&to=2026-10-19T00:00:00Z HTTP/1.1
Host: {API_HOST}
X-Operator: acme
X-Api-Key: ak_live_…
X-Timestamp: 1760839200123
X-Nonce: 7c9e1a3b5d7f9a1c3e5b7d9f
X-Signature: 52aa…Response example
{
"currency": "PHP",
"rows": [
{ "day": "2026-10-18", "game_code": "live_baccarat", "rounds": 640, "players": 57, "bet": 18250000, "win": 17310000, "rake": 0, "ggr": 940000 },
{ "day": "2026-10-18", "game_code": "live_holdem", "rounds": 1203, "players": 81, "bet": 0, "win": 0, "rake": 412000, "ggr": 412000 }
]
}Response fields
| Field | Type | Description |
|---|---|---|
currency | string | Currency of the amounts (see the note below) |
rows[].day | string | Date YYYY-MM-DD — the settlement time cut by UTC date |
rows[].game_code | string | Game code |
rows[].rounds | int64 | Number of rounds |
rows[].players | int64 | Number of participating players (distinct within that day and game) |
rows[].bet, win | int64 | Total bets and total payouts for banked games. 0 for P2P games |
rows[].rake | int64 | Rake attributed to this operator's players' share in P2P games. 0 for banked |
rows[].ggr | int64 | bet − win + rake |
- For P2P games (Hold'em, etc.), GGR is the rake. Chips exchanged between players are not included in GGR.
- For banked games (Baccarat, etc.), GGR is
bet − win. - Rows are ordered by date, then game code. Voided rounds do not affect the GGR amounts, but depending on the game they may be counted in
roundsandplayers.
Currency
All amounts are integers in the currency's minor unit (e.g. for PHP, centavos: 1250000 = ₱12,500.00). Check the number of decimal places with exponent of GET /currencies.
The response has only one currency, and rows has no currency column. Aggregation is done separately per currency internally, so for operators using tables in two or more currencies, the same date/game row appears multiple times, once per currency, and they cannot be told apart. If you use multiple currencies, aggregate per-currency figures yourself with GET /rounds (currency on every row).
Errors
| HTTP | Code | Situation |
|---|---|---|
| 500 | INTERNAL_ERROR | Temporary server error |
Reconciliation
Because the operator wallet is the source of truth for player balances, we recommend regularly checking that the platform ledger (GET /transactions) and the operator wallet ledger match transaction by transaction. The common key between the two ledgers is tx_id.
Preparation — what to keep in the operator wallet ledger
When processing wallet callbacks (debit, credit, rollback), store at least the following.
| Item | Reason |
|---|---|
tx_id (unique key) | Reconciliation key and idempotency key |
type, amount, currency, external_id | Comparing amounts and targets |
ref_tx_id (rollback) | Which debit was cancelled |
round_id, reason | Tracing the cause (if tss_…, you can follow up with GET /sessions) |
| Processing result (applied / rejected / ignored) | Whether a debit was rejected, or a rollback did nothing because there was no target |
The operator_tx_id you responded with | Cross-check against operator_tx_id in the platform ledger |
Procedure
- Choose a range. E.g. all of yesterday,
[2026-10-18T00:00:00Z, 2026-10-19T00:00:00Z).toshould be at least a few minutes in the past so that in-flight transactions appear in their finished state. Fetch with a fixedfromandto. - Fetch the entire platform ledger. Repeat until
next_cursorbecomesnull, as in the example code above. - Prepare the operator ledger for the same range. The platform's
created_atis when the platform created the transaction; the time the operator received the request may be slightly later (on retries) or much later. To avoid missing rows at the boundaries, extract the operator side with a margin before and after the range (e.g. ±1 hour), then check separately only those not found among the platform rows. - Match by
tx_id. Judge each platform row by the rules below.
Platform type / status | Expected state in the operator ledger | If it does not match |
|---|---|---|
debit / ok | The same amount debited with the same tx_id | Anomaly if there is no debit record or the amount differs — the platform has already granted chips / accepted the bet |
debit / failed | Not debited (only a rejection record, or no record because the call was never made due to insufficient prepaid credit) | If it was debited, the player's money was taken but not reflected in the game — a refund is needed |
debit / uncertain | Whether or not it was debited, combined with the rollback whose ref_tx_id is this debit, the net change is zero | Anomaly if it was debited but the rollback was not applied |
rollback / ok | Reversed if the original debit was applied; if the original debit was never received, do nothing and return OK | Anomaly if the original debit remains applied |
credit / ok | The same amount credited with the same tx_id (also record settle with amount 0) | Anomaly if there is no credit record or the amount differs |
All types / pending | Not final yet | Check again at the next reconciliation. Credits and rollbacks are retried until they receive OK, so check the wallet response status |
- Check the opposite direction as well. A
tx_idthat exists in the operator ledger but not in the platform ledger (even over the wider range) may be a request that came from another environment or another operator code, or one that bypassed signature verification — investigate it. - Verify totals (optional). For each player,
Σ(ok debit) − Σ(ok rollbacks whose original debit was ok) − Σ(ok credit)should equal the same calculation on the operator side.
Additional checks for buy-in flow games
Buy-in flow games such as Hold'em have no per-round wallet transactions, so when a player reports that "chips disappeared", check at the table session level.
- Get the
round_id(tss_…) from that player'sbuy_in,top_up, andcash_outtransactions. - Look at
reconcile.balancedof GET /sessions/{session_id}. Iftrue, buy-in + per-round profit/loss = cash-out. - For per-round details, query the round details with each
round_idinrounds[].
To be agreed
The reconciliation schedule, and the adjustment procedure and contact point when a mismatch is found, will be agreed upon at contract time.