OrbiPoker developer documentation · API v1
Real-money poker inside your casino, with one URL and one webhook.
OrbiPoker runs the whole poker room: game engine, lobby, cash tables, fast-fold, Sit & Go, Spin & Go and tournaments, back office and integrity. You keep the player account, KYC and money. The game opens in an iframe on your site, and every chip movement is a signed call to your wallet.
Architecture
- Money stays with you. OrbiPoker never holds balances. Chips on a table or a tournament entry exist because your wallet accepted a debit.
- Integer minor units. Every amount is an integer in the currency's smallest unit (cents). Tournament chips are plain integers and never touch the wallet.
- Network or private. Your players can share tables with other operators (network liquidity) or only meet each other (private). This is a per-operator setting.
- Three languages. The game client speaks pt-BR, en and es.
Quick start
From zero to a hand dealt on your staging site. OrbiPoker staff create your operator account and send you an API key (orbp_sk_…) and a wallet secret (whsec_…) once.
- Try the sandbox. Open /sandbox: it acts as an operator backend for the demo tenant, so you can see launches, iframe events and wallet calls without writing code.
- Run the reference wallet. Download wallet-receiver.mjs (Node 24, no dependencies) and run it with your secret. Expose it over HTTPS.
- Configure the back office. In /backoffice set the wallet URL, add the origins that will embed the iframe and press “Test wallet”.
- Create a launch and embed it. Call
POST /api/v1/launchfrom your server and mount the returned URL with the SDK. - Play a hand. Sit at a table: your wallet receives a
debit. Leave: it receives acredit.
# 1. the reference wallet (keep the secret out of shell history in production)
ORBIPOKER_WALLET_SECRET=whsec_… PORT=3001 START_BALANCE=100000 node wallet-receiver.mjs
# 2. a launch for player 42
curl -X POST "$ORBIPOKER_URL/api/v1/launch" \
-H "Authorization: Bearer $ORBIPOKER_API_KEY" -H "Content-Type: application/json" \
-d '{"playerId":"42","currency":"BRL","locale":"pt-BR","parentOrigin":"https://staging.casino.example"}'
Authentication
Server-to-server calls use an operator API key in the Authorization header. Keys start with orbp_sk_, are shown once when created, are stored only as a SHA-256 hash, and can be created and revoked in the back office (Integration → API keys). Never send a key to a browser or an app.
Authorization: Bearer orbp_sk_9xQ…
| Status | Code | Meaning |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, malformed, unknown or revoked key. |
| 403 | TENANT_SUSPENDED | Your operator account is suspended. Player sessions are revoked too. |
| 429 | RATE_LIMITED | More than 600 requests per minute from one IP across /api/*. Honour Retry-After: 60. |
Errors are JSON: {"error":"human message","code":"MACHINE_CODE"}, sometimes with extra fields such as min and max. Branch on code, never on the message. Every API call is written to the audit log with method, path and duration.
The wallet webhook works the other way round: OrbiPoker calls you and signs each request with your wallet secret. See Seamless wallet.
Launch API
Call this from your backend each time a logged-in player opens poker. It creates (or updates) the player and returns a single-use URL for the iframe.
POST /api/v1/launch
Authorization: Bearer orbp_sk_…
Idempotency-Key: open-poker-42-1792443000
Content-Type: application/json
{
"playerId": "42",
"nickname": "AnaRiver",
"currency": "BRL",
"locale": "pt-BR",
"country": "BR",
"parentOrigin": "https://casino.example",
"sessionToken": "your-session-reference",
"lobby": "spin",
"depositUrl": "https://casino.example/cashier",
"exitUrl": "https://casino.example/casino"
}
201 Created
{ "launchId": "lc_3f…", "launchUrl": "https://…/launch/lc_3f….Gx2…", "expiresAt": 1792443120000 }
| Field | Rule | Description |
|---|---|---|
playerId required | 1–128 · [A-Za-z0-9_.@:+-] | Your stable player id. It comes back as playerId in every wallet call. |
currency required | enabled for your account | The player's wallet currency. Tables and tournaments are filtered by it. |
nickname | 3–16 · [\p{L}\p{N}_.-] | Used on the first launch only. Nicknames are unique across the network; a numeric suffix is added on collision. Defaults to a cleaned playerId. |
locale | pt-BR · en · es | Client language. Defaults to the player's last locale, then your defaultLocale. |
country | ISO 3166-1 alpha-2 | Shown as a flag at the table. |
parentOrigin | must be in your allow-list | The origin of the page that embeds the iframe. The client only posts events to this origin. |
sessionToken | string ≤ 512 | Opaque value echoed in every wallet call for this session, so you can tie movements to your own session. |
lobby | cash · fastfold · sng · spin · mtt | Opens the lobby on that tab, for example from a “Spin & Go” tile in your casino lobby. |
table | table id ≤ 64 | Opens a specific table. |
theme | same schema as config.theme | Per-session theme override, validated like the operator theme. |
depositUrl · exitUrl | http(s) URL | Where the client's Deposit and Exit buttons point when there is no host page listening to events. |
Rules
- Single use, 120 seconds. The URL expires two minutes after creation and is consumed when the iframe loads it. Create a new launch every time the player opens poker; never cache or share launch URLs.
- Player session. Consuming the URL issues a player token valid for 12 hours. Reloading the iframe keeps the session; when it expires the client emits
orbipoker:session-expiredand you create a new launch. - Idempotency-Key. Optional, 8–100 characters of
[A-Za-z0-9_-]. Repeating the call with the same key returns the same launch; the same key with a differentplayerIdfails with409 IDEMPOTENCY_CONFLICT. - Origins.
parentOriginmust exactly match one of your allow-listed origins (HTTPS, no path; plain HTTP only for localhost), otherwise403 ORIGIN_NOT_ALLOWED. - Framing (CSP).
/launch/<code>is served withContent-Security-Policy: frame-ancestors 'self' <your allow-listed origins>, so browsers refuse to render it anywhere else. Sandbox accounts allow any origin. - Blocked players. A blocked or self-excluded player gets
403 PLAYER_BLOCKEDuntil the block ends. - Strict body. Unknown fields are rejected with
400 INVALID_BODY, so typos fail loudly.
| Status | Code | Meaning |
|---|---|---|
| 400 | INVALID_PLAYER · INVALID_NICKNAME · INVALID_LOCALE · INVALID_COUNTRY · INVALID_URL · INVALID_LOBBY · INVALID_THEME · INVALID_IDEMPOTENCY_KEY | A field failed validation. |
| 400 | INVALID_CURRENCY | The currency is not enabled for your account. |
| 403 | ORIGIN_NOT_ALLOWED · PLAYER_BLOCKED | See the rules above. |
| 409 | IDEMPOTENCY_CONFLICT | Key reused for a different player. |
Iframe and SDK
Put the launch URL in an iframe. The SDK does it for you and turns the client's postMessage events into callbacks.
With the SDK (recommended)
<div id="poker"></div>
<script src="https://<orbipoker-host>/sdk/v1/orbipoker.js"></script>
<script>
const poker = OrbiPoker.mount({
container: '#poker', // selector or element
launchUrl, // from POST /api/v1/launch
onReady: info => console.log(info.playerId, info.locale),
onBalance: ({balance, currency}) => header.setBalance(balance, currency),
onDeposit: () => cashier.open(),
onExit: () => location.assign('/casino'),
onSessionExpired: () => relaunch(), // create a new launch
onError: ({code, message}) => log(code, message),
onEvent: event => analytics.track(event.type, event)
});
poker.refreshBalance(); // after a deposit in your cashier
poker.setLocale('es'); // pt-BR | en | es
poker.destroy(); // removes the iframe and listeners
</script>
- Load the SDK from the OrbiPoker host. It only accepts launch URLs from the same origin it was served from, and only trusts messages whose
originandsourceare that iframe. - The iframe height follows
orbipoker:resize(320–20,000 px) unless you passheight(number of px or any CSS length). Default height is 720 px. mount()returns{iframe, refreshBalance(), setLocale(locale), destroy()}. It throws if the container is missing or the launch URL is not an OrbiPoker/launch/URL.
Plain iframe
<iframe src="{launchUrl}" title="Poker" allow="autoplay; fullscreen; clipboard-write"
style="border:0;width:100%;height:720px"></iframe>
<script>
const ORBIPOKER = 'https://<orbipoker-host>', frame = document.querySelector('iframe');
addEventListener('message', e => {
if (e.origin !== ORBIPOKER || e.source !== frame.contentWindow) return;
const m = e.data;
if (m?.source !== 'orbipoker' || m.version !== 1) return;
if (m.type === 'orbipoker:resize') frame.style.height = m.height + 'px';
});
// host → iframe
frame.contentWindow.postMessage({source: 'orbipoker-host', version: 1, type: 'orbipoker:refresh-balance'}, ORBIPOKER);
</script>
Events (iframe → host)
Every message is {source: 'orbipoker', version: 1, type, …fields}, posted only to your parentOrigin (to * only in the public demo).
| type | Fields | When | SDK |
|---|---|---|---|
orbipoker:ready | playerId, nickname, currency, locale, tenantId | The session is authenticated and the lobby is shown. | onReady |
orbipoker:resize | height | Content height changed. | automatic |
orbipoker:balance | balance, currency | The wallet balance changed after a debit or credit. Update your header. | onBalance |
orbipoker:deposit | — | The player pressed Deposit. Open your cashier. | onDeposit |
orbipoker:exit | — | The player pressed Exit. | onExit |
orbipoker:session-expired | — | The player token expired or was revoked. Create a new launch. | onSessionExpired |
orbipoker:error | code, message | A fatal client error (for example LAUNCH_EXPIRED). | onError |
orbipoker:table-opened | tableId | The player opened a table. | onEvent |
orbipoker:hand-complete | tableId, handId, net | A hand the player was dealt into finished; net is their result in minor units. | onEvent |
Messages (host → iframe)
{source: 'orbipoker-host', version: 1, type} with type = orbipoker:refresh-balance (re-read the wallet balance) or orbipoker:locale plus locale. Always target the OrbiPoker origin, never *.
Seamless wallet
You implement one HTTPS endpoint. OrbiPoker calls it to read the balance, to take money when chips are bought (debit), to return money when chips come back (credit) and to undo a debit whose outcome is unknown (rollback).
Request
POST https://wallet.casino.example/orbipoker
Content-Type: application/json
User-Agent: OrbiPoker-Wallet/1
X-OrbiPoker-Signature: t=1792443000,v1=5105527f40f453f1716c2efc94ad7f9c7498186b546bf6c0924dcbde3b3dab50
{"version":1,"event":"debit","transactionId":"0d3c2f9e-8a1b-4c7e-9f1a-2b6d5e4c3a10","referenceTransactionId":null,
"tenantId":"7b1e…","playerId":"42","currency":"BRL","amount":5000,"reason":"cash_buyin",
"tableId":"t_9f…","tournamentId":null,"handId":null,"sessionToken":"your-session-reference","createdAt":1792443000123}
200 OK
{"balance": 120000}
| Field | Type | Description |
|---|---|---|
version | 1 | Payload version. |
event | balance · debit · credit · rollback | What to do. |
transactionId | UUID | Idempotency key. Unique per movement and identical on every retry. |
referenceTransactionId | UUID | null | For rollback: the debit being reversed. |
tenantId | string | Your operator id. Reject anything that is not yours. |
playerId | string | Your player id from the launch. |
currency | string | The player's currency. |
amount | integer ≥ 0 | Minor units. Absent on balance. |
reason | string | Why the money moves (table below). Store it for reporting. |
tableId · tournamentId | string | null | Where the chips are. Fast-fold uses the pool id as tableId. |
handId | string | null | Reserved; currently always null. |
sessionToken | string | null | The sessionToken you passed at launch. It may be null for movements made after the session ended (recovery, refunds, prizes). |
createdAt | ms | When OrbiPoker recorded the movement (not when it was sent). |
Signature
X-OrbiPoker-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, t + "." + rawBody)>. Compute the HMAC over the raw bytes you received, before any JSON parsing, compare in constant time, and reject timestamps more than 300 seconds from your clock. Answer 401 on failure. Rotate the secret in the back office; the new secret applies immediately.
import {createHmac, timingSafeEqual} from 'node:crypto';
export function verifyOrbiPoker(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(String(header || '').split(',').map(p => p.split('=')));
const t = Number(parts.t);
if (!Number.isInteger(t) || !/^[0-9a-f]{64}$/.test(parts.v1 || '')) return false;
if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = createHmac('sha256', secret).update(t + '.' + rawBody).digest();
return timingSafeEqual(expected, Buffer.from(parts.v1, 'hex'));
}
// Express: app.post('/orbipoker', express.raw({type: 'application/json'}), (req, res) => …)
// verifyOrbiPoker(req.body.toString('utf8'), req.get('X-OrbiPoker-Signature'), process.env.ORBIPOKER_WALLET_SECRET)
function verify_orbipoker(string $rawBody, string $header, string $secret, int $tolerance = 300): bool {
$parts = [];
foreach (explode(',', $header) as $pair) {
[$k, $v] = array_pad(explode('=', $pair, 2), 2, '');
$parts[trim($k)] = trim($v);
}
if (!isset($parts['t'], $parts['v1']) || !ctype_digit($parts['t'])) return false;
if (abs(time() - (int) $parts['t']) > $tolerance) return false;
$expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret);
return hash_equals($expected, $parts['v1']);
}
$raw = file_get_contents('php://input'); // raw body, before json_decode
if (!verify_orbipoker($raw, $_SERVER['HTTP_X_ORBIPOKER_SIGNATURE'] ?? '', getenv('ORBIPOKER_WALLET_SECRET'))) {
http_response_code(401); exit;
}
import hmac, hashlib, time
def verify_orbipoker(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.split('=', 1) for p in header.split(',') if '=' in p)
t, v1 = parts.get('t', ''), parts.get('v1', '')
if not t.isdigit() or abs(time.time() - int(t)) > tolerance:
return False
expected = hmac.new(secret.encode(), t.encode() + b'.' + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)
# Flask: verify_orbipoker(request.get_data(), request.headers.get('X-OrbiPoker-Signature', ''), SECRET)
Responses
| Event | Accept | Non-2xx or timeout |
|---|---|---|
balance | 2xx with {"balance": integer} (required). | The client shows the balance as unavailable. Nothing is retried. Do not store these requests: their transactionId is random. |
debit | 2xx after you have taken the money. Return the new balance so the client can update. | Declined. No chips are given and the debit is never retried. Use 402 for insufficient funds; an optional message in your JSON is shown to the player. A timeout or network error is different: see rollback. |
credit | 2xx after you have added the money (or if you already had). | Retried with the same body until you answer 2xx (see the schedule below). The player's chips are already off the table; the money is owed to them. |
rollback | 2xx after restoring the referenced debit, or after recording a tombstone if you never saw it. | Retried like a credit. |
Idempotency
- Deduplicate by
transactionIdin the same database transaction as the balance change. A retry carries a byte-identical body. - A duplicate gets the same outcome as the original: 2xx with the current balance if it was applied, the same decline if it was declined. Never apply it twice.
- Store a hash of the raw body. The same
transactionIdwith a different body is a bug or an attack: answer409and alert. - Process deliveries for one player serially (row lock or
BEGIN IMMEDIATE); credits and rollbacks may arrive concurrently with a new debit.
Rollback semantics
OrbiPoker sends a rollback immediately when a debit times out (8 s) or fails at the network level, because it cannot know whether you applied it. The player is told nothing was charged. referenceTransactionId is the debit's transactionId, amount is the debit amount and reason is debit_timeout.
- Debit applied: restore exactly the debited amount once, mark the debit as rolled back, answer 2xx.
- Debit never seen (the rollback overtook it, or it never arrived): store a tombstone for
referenceTransactionIdand answer 2xx without moving money. - Debit arrives after its tombstone: refuse it with a non-2xx status (for example 409). Money must not move.
- Debit already rolled back or declined: answer 2xx and change nothing.
Delivery, timeouts and retries
- Timeout: 8 seconds per attempt. Redirects are not followed. The response body is read up to 16 KB.
- Every movement is written to OrbiPoker's database (outbox) before it is sent, so nothing is lost on a restart.
- Credits and rollbacks: first attempt immediately, then exponential backoff of 2 s × 2attempt capped at 30 minutes (2 s, 4 s, 8 s … 17 min, 30 min, 30 min …) for 24 hours. After that the transaction is marked
failedand shows in the back office for manual reconciliation (POST /api/v1/transactions/{id}/retryresends the same body). - Debits are synchronous and never retried: the player waits for your answer before sitting down.
- The wallet URL must be HTTPS and resolve to a public address; private and loopback addresses are refused (sandbox accounts excepted).
| Status | Description |
|---|---|
pending | Recorded, first attempt in flight. |
ok | You answered 2xx. |
declined | Debit refused with a non-2xx status. |
unknown | Debit timed out; a rollback was queued. |
retrying | Credit or rollback waiting for its next attempt. |
failed | Gave up after 24 h. Reconcile manually. |
When money moves
| reason | Event | When |
|---|---|---|
| Cash tables | ||
cash_buyin | debit | The player sits down (or is seated from the wait list) with the chosen buy-in. |
cash_topup | debit | Manual or automatic top-up. Chips added mid-hand play from the next hand. |
cash_cashout | credit | The player leaves (after the current hand if still in it), is removed for sitting out, busts and does not top up within 2 minutes, or is kicked/blocked by you. Also used to return a buy-in when the seat was taken while the debit was in flight. |
cash_table_closed | credit | You or OrbiPoker closed the table; stacks are returned after the current hand. |
cash_recovery | credit | After a server restart: the stack at the last completed hand is returned and the interrupted hand is void. |
| Fast-fold | ||
fastfold_buyin · fastfold_topup | debit | Joining a pool, or topping up between hands. |
fastfold_cashout | credit | Leaving the pool, removal after sitting out, or pool closed. |
| Sit & Go, Spin & Go and MTT | ||
tourney_buyin | debit | Registration or re-entry: buy-in + fee + bounty in one debit. |
tourney_refund | credit | Unregistering before the start, or a tournament cancelled before it starts (including an MTT that misses its minimum players). |
tourney_prize | credit | Finishing in the money (sent when the player busts in a paid place, or at the end for the winner, including the winner's own bounty in PKO). |
tourney_bounty | credit | PKO: the cash half of a bounty, sent immediately after the knockout. |
tourney_recovery | credit | After a server restart during a running tournament: the remaining prize pool split by chip count plus each player's current bounty. |
| Other | ||
debit_timeout | rollback | A debit that timed out or failed at the network level. |
Minor units follow the currency's decimals: 2 for USD, EUR, BRL, GBP, MXN, ARS, PEN, CAD, USDT, INR, PHP and CR; 0 for CLP and COP. 5000 BRL is R$ 50,00; 5000 CLP is CLP$ 5.000.
The reference receiver implements all of the above in about 140 lines of dependency-free Node (node:http + node:sqlite), and the repository's test suite checks it against the real signer.
Operator REST API
Every endpoint below takes Authorization: Bearer orbp_sk_…, speaks JSON and is scoped to your operator. This table is generated live from /openapi.json (OpenAPI 3.1), which is the source of truth for request schemas.
| Method | Path | Summary |
|---|---|---|
| Loading /openapi.json… | ||
Notes
GET /api/v1/players/{playerId}accepts yourplayerIdand returns status, limits, lifetime stats, recent hands, tournaments, wallet movements andinPlay(chips currently on tables and active tournament entries).POST /api/v1/tablescreates a private table for your players, or a shared one with"network": trueif your liquidity isnetwork. Fields:name(2–40),kind(cash|fastfold),variant,currency,bb, optionalsb(default bb/2),ante,bbAnte,seats(2, 6, 8, 9; Short Deck, PLO5 and PLO6 up to 6),minBuyIn/maxBuyIn(default 40/100 BB, allowed 10–1,000 BB),rakeBps,rakeCap(absolute, minor units) andfeatures(straddle,runItTwice,rabbitHunt,anonymous).POST /api/v1/tournamentscreates an MTT (one-off unlesstemplate: truewithrecurringMinutes) or a Sit & Go / Spin template that keeps one instance open at all times. See Games and rules for defaults.DELETEcancels a tournament that has not started (everyone is refunded) or archives a template (tt_…).GET /api/v1/hands/{handId}?format=textreturns a PokerStars-style hand history. Operator hand views include every hole card, for integrity reviews.- Timestamps are Unix milliseconds. List endpoints take
limit(max 500) and, where noted,offset.
Configuration
Read with GET /api/v1/config, change with PATCH /api/v1/config (deep-merged) or in the back office. Validation is strict: unknown top-level keys, unknown colours or features, fonts outside the list and out-of-range numbers are rejected with 400. No raw CSS ever reaches the client.
PATCH /api/v1/config
{ "theme": { "preset": "classic", "font": "Montserrat" },
"timers": { "actionSeconds": 20 },
"features": { "spins": true, "hud": false },
"rake": { "bps": 450 } }
| Key | Default | Allowed | Description |
|---|---|---|---|
| brand | |||
brand.name | operator name | 2–40 | Name shown in the client. |
brand.logoUrl | null | HTTPS URL | Logo in the lobby header. |
brand.lobbyTitle | "Poker" | string | Lobby title. |
| theme | |||
theme.preset | "midnight" | midnight · classic · royal · light | Sending a preset without colors loads that preset's palette. |
theme.colors.* | midnight palette | #rrggbb | 15 tokens: bg surface surfaceAlt text muted accent accentText felt feltEdge cardBack danger success fold call raise. text must have 4.5:1 contrast with bg. |
theme.font | "Inter" | Inter · Manrope · Roboto · Montserrat · Poppins · system-ui | Client font. |
theme.radius | 12 | 0–24 | Corner radius in px. |
theme.deck | "four-color" | four-color · two-color | Suit colours. |
theme.cardFace | "classic" | classic · large-index | Card face style; large index reads better on phones. |
theme.tableShape | "oval" | oval · rounded | Table shape. |
theme.animations · theme.sounds | true | boolean | Defaults for new players; each player can change them. |
| features | |||
features.cash · fastFold · sitAndGo · spins · mtt | true | boolean | Formats your players can join (enforced by the server). |
features.chat · emotes · notes · hud | true | boolean | Chat, emotes, player notes and the built-in HUD (enforced by the server). |
features.straddle · runItTwice · rabbitHunt | true | boolean | Allowed on your tables; each table must also enable it. |
features.allInEquity · handHistory · replayer · provablyFair · waitlist · autoTopUp · showLeaderboards · multiTable | true | boolean | Client features shown to players. |
features.anonymousTables | false | boolean | Default for anonymous tables in the client. Anonymity itself is a per-table flag. |
| timers | |||
timers.actionSeconds | 15 | 5–60 | Time to act at cash tables. |
timers.fastFoldActionSeconds | 10 | 5–30 | Time to act in fast-fold. |
timers.tournamentActionSeconds | 12 | 5–60 | Time to act in tournaments. |
timers.timeBankSeconds | 15 | 0–300 | Starting time bank; used automatically when the action clock runs out. |
timers.timeBankMaxSeconds | 60 | 0–600 | Maximum time bank. |
timers.timeBankRefillSeconds | 5 | 0–60 | Seconds added every timeBankRefillHands hands. |
timers.timeBankRefillHands | 10 | 1–1000 | Hands per refill. |
timers.sitOutKickMinutes | 10 | 1–120 | A player sitting out longer than this is cashed out and leaves the table. |
timers.handPauseMs | 2500 | 500–10000 | Pause between hands at cash tables. |
timers.missedBlindsSitOut | 2 | 1–10 | Reserved; not used yet. |
| limits · betting | |||
limits.maxTables | 4 | 1–12 | Cash tables plus fast-fold pools a player can play at once. A per-player limit can only lower it. |
limits.maxBuyInPerDay | null | minor units | Default daily buy-in cap for every player (UTC day). A per-player limit replaces it. |
limits.sessionReminderMinutes | 60 | 0–1440 | Reality-check reminder shown by the client. |
betting.preflopRaises | [2.2, 2.5, 3, 4] | ≤ 6 values, 1–20 BB | Preflop raise buttons, in big blinds. |
betting.postflopPotPercents | [33, 50, 75, 100] | ≤ 6 integers, 10–300 | Postflop bet buttons, in % of the pot. |
betting.showPotButton | true | boolean | Show a Pot button. |
| rake · fees · promotions | |||
rake.bps | 500 | 0–1000 | Cash rake in basis points (500 = 5%). A table's rakeBps overrides it. |
rake.noFlopNoDrop | true | boolean | No rake when the hand ends before the flop. |
rake.capsByPlayers | {"2":100,"4":200,"10":300} | integer keys and values | Cap per hand by players dealt in: the first key ≥ the player count applies. With capUnitBb the values are hundredths of a big blind (1, 2 and 3 BB). |
rake.capUnitBb | true | boolean | Caps in BB/100 (true) or minor units (false). A table's rakeCap is always an absolute amount. |
tournamentFeeBps | 1000 | 0–2500 | Default MTT and Sit & Go fee as a share of the buy-in, when a tournament does not set fee. |
spinFeeBps | 700 | 700 | 800 | Spin margin: 700 = GGPoker multiplier table (93% RTP), 800 = PokerStars table (92% RTP). Other values are rejected. |
promotions.cashDrop | {"enabled":false,"oneInHands":150,"minBb":5,"maxBb":30} | oneInHands 10–100000 · 1 ≤ minBb ≤ maxBb ≤ 500 | Cash Drops: on average one hand in oneInHands gets minBb–maxBb big blinds of your money added to the pot. Your private cash and fast-fold tables only; never raked; reported as promoCost. |
| locales · chat | |||
locales | ["pt-BR","en","es"] | non-empty subset | Languages offered in the client. |
defaultLocale | "pt-BR" | one of locales | Language for new players without a locale. |
chat.maxLength · chat.rateSeconds | 120 · 2 | 10–500 · 0–60 | Message length and one message per N seconds. |
chat.bannedWords | [] | ≤ 500 words, 1–40 chars | Words replaced with *** (case-insensitive). |
Games and rules
Defaults follow PokerStars and GGPoker, so regular players feel at home. The reasoning behind each default, with the market research, is in docs/FEATURES.md in the repository.
Variants
| Code | Game | Notes |
|---|---|---|
NLHE | No-Limit Hold'em | 2 hole cards. |
PLHE | Pot-Limit Hold'em | Pot-limit betting. |
PLO4 · PLO5 · PLO6 | Pot-Limit Omaha | 4, 5 or 6 hole cards; exactly two from hand and three from the board. PLO5/PLO6 up to 6 seats. |
PLO8 | Omaha Hi/Lo | Eight-or-better low; pots split high/low, odd chip to the high. |
SHORT | Short Deck (6+) | 36 cards; flush beats full house, trips beat straight, A-6-7-8-9 is the lowest straight. Up to 6 seats. |
Rake
Cash and fast-fold hands pay rake.bps of the pot, floored to the minor unit and capped by the number of players dealt in: by default 1 BB heads-up, 2 BB with 3–4 players and 3 BB with 5 or more. With no flop, no drop. Tournaments pay no rake: the fee is charged at registration. Rake is attributed to each player in proportion to what they put in the pot (contributed method); that is what per-player and per-operator reports use.
Table rules
- Timers and time bank. When the action clock runs out the time bank starts automatically. When both run out the player checks if possible, otherwise folds and is sat out. The bank grows by
timeBankRefillSecondseverytimeBankRefillHandshands up totimeBankMaxSeconds. - Run it twice. Cash tables only (not fast-fold or tournaments). When everyone is all-in before the river and every all-in player keeps the option on (default on), two boards are dealt and each pot is split between them.
- Straddle. When the table allows it, the player under the gun can opt in to post 2 BB blind (3+ players, cash only). Action starts left of the straddle.
- Antes. Cash tables may have a classic ante (everyone) or a big-blind ante (one ante of 1 BB from the big blind). Blinds take priority when a stack cannot cover both.
- Wait for big blind. A player joining a running table is dealt in when the big blind reaches them.
- Rathole protection. Returning to the same table within 30 minutes requires at least the stack you left with (up to 3 × the max buy-in).
- Sit-out and busts. Sitting out longer than
sitOutKickMinutescashes the player out. A busted player has 2 minutes to top up. - Same network. Two players on the same IP address cannot sit at the same cash table (
SAME_NETWORK); the attempt raises an integrity alert. - Rabbit hunt and equity. After an uncontested pot the undealt board is shown for free. All-in equity is shown when players are all-in before the river.
- Fast-fold. Players join a pool and are dealt a new table every hand. Folding works before your turn. The big blind goes to whoever has gone longest without posting it; seats are shuffled; pools are anonymous by default.
Tournaments
| Sit & Go | Spin & Go | MTT | |
|---|---|---|---|
| Seats | 2–9 (6) | 3 | 2–9 (8) per table |
| Start stack / BB | 1,500 / 20 | 500 / 20 | 10,000 / 100 |
| Speed (level) | turbo (5 min) | 2–5 min by multiplier | regular (10 min) |
| BB ante from level | 4 | never | 3 |
| Fee | tournamentFeeBps | in the multiplier table | tournamentFeeBps |
| Starts | when full | when full | startAt |
- Speeds: slow 15 min, regular 10 min, turbo 5 min, hyper 3 min per level (override with
levelSeconds, 60–3,600). Blinds climb a fixed ladder of 1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10… × the starting big blind. - MTT registration: late registration for
lateRegLevelslevels (default 8), closing early if 15% or fewer entrants remain. Re-entry up tomaxEntries(1–10) while late registration is open. If fewer thanminPlayersare registered atstartAt, the tournament is cancelled and refunded. Tables are balanced and broken between hands. - Payouts: Sit & Go pays 100% up to 3 seats, 65/35 up to 6 and 50/30/20 above. MTTs pay about 15% of entries on a decreasing curve with a min-cash of at least 1.5 buy-ins, fixed when late registration closes. Players who bust during late registration finish out of the money. If the pool is smaller than the guarantee, the guarantee is paid.
- PKO: each entry carries a bounty. Knocking a player out wins their bounty: half in cash right away (
tourney_bounty), half added to your own bounty (setprogressiveBounty: falsefor a regular knockout). The winner also collects their own bounty. - Crash policy: stacks are saved after every hand. If the server restarts during a tournament, the remaining prize pool (or the guarantee) is split by chip count, each player gets their current bounty, and the tournament is closed. Cash tables return the stack from the last completed hand.
Spin & Go multipliers
The multiplier is drawn when the Spin fills. The prize pool is multiplier × buy-in. The expected multiplier is 3 × (1 − fee), so RTP is 93.0% on the 7% table (GGPoker Spin & Gold) and 92.0% on the 8% table (PokerStars Spin & Go).
| Multiplier | Per 1,000,000 (7%) | Per 1,000,000 (8%) | Prizes (1st / 2nd / 3rd) | Level |
|---|---|---|---|---|
| 12,000× | 1 | 1 | 83.3 / 8.3 / 8.3% | 5 min |
| 240× | 30 | 30 | 83.3 / 8.3 / 8.3% | 5 min |
| 120× | 75 | 75 | 83.3 / 8.3 / 8.3% | 5 min |
| 25× | 1,000 | 1,000 | 80 / 10 / 10% | 4 min |
| 10× | 5,000 | 5,000 | 80 / 20% | 4 min |
| 5× | 85,000 | 85,000 | 100% | 3 min |
| 3× | 444,000 | 414,012 | 100% | 3 min |
| 2× | 464,894 | 494,882 | 100% | 2 min |
Provably fair shuffle
- Before each hand the server draws a 32-byte
serverSeedfrom the operating system's CSPRNG and publishescommitment = SHA-256(serverSeed bytes)in the table view. - The deck (52 cards, or 36 for Short Deck, ordered
2c 2d 2h 2s 3c … As) is shuffled with Fisher-Yates from the last position down. Random numbers are the big-endian 32-bit words ofHMAC-SHA256(key = serverSeed, message = handId + ":" + counter)for counter = 0, 1, 2…; a word is rejected and the next one used when it is ≥ ⌊2³²/range⌋ × range, so every permutation is equally likely. - Cards are dealt from the top of the shuffled deck with no burn cards: hole cards one at a time to each player starting with the button seat, then flop, turn and river (and a second board when running it twice).
- When the hand ends,
serverSeedis revealed in the result, the hand history and the API, so anyone can rebuild the deck.
// Runs in any modern browser or Node ≥ 20 (WebCrypto).
async function verifyHand({serverSeed, commitment, handId, deck, shortDeck = false}) {
const hex = buf => [...new Uint8Array(buf)].map(b => b.toString(16).padStart(2, '0')).join('');
const seed = Uint8Array.from(serverSeed.match(/../g), h => parseInt(h, 16));
if (hex(await crypto.subtle.digest('SHA-256', seed)) !== commitment) return false;
const key = await crypto.subtle.importKey('raw', seed, {name: 'HMAC', hash: 'SHA-256'}, false, ['sign']);
let counter = 0, words = [];
async function next() {
if (!words.length) {
const block = new DataView(await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(handId + ':' + counter++)));
for (let i = 0; i < 32; i += 4) words.push(block.getUint32(i));
}
return words.shift();
}
const cards = [];
for (let c = 0; c < 52; c++) if (!shortDeck || (c >> 2) >= 4) cards.push(c); // rank = c >> 2, suit = c & 3
for (let i = cards.length - 1; i > 0; i--) {
const range = i + 1, limit = Math.floor(2 ** 32 / range) * range;
let value; do value = await next(); while (value >= limit);
const j = value % range; [cards[i], cards[j]] = [cards[j], cards[i]];
}
const name = c => '23456789TJQKA'[c >> 2] + 'cdhs'[c & 3];
return cards.map(name).join(' ') === deck.join(' '); // deck from the "verify" message
}
Players get commitment, serverSeed and the full deck from the client's hand history (“Verify”). Operators get commitment and serverSeed from GET /api/v1/hands/{handId}.
Responsible gaming
You own the player relationship, so you drive these controls from your own tools. All of them are audited.
| Call | Description |
|---|---|
POST /api/v1/players/{id}/block{"reason":"self-exclusion","until":1800000000000} | Blocks the player (until a date, or indefinitely with null). Their sessions are revoked, launches fail with PLAYER_BLOCKED, they are removed from cash tables and pools and their chips are credited back. Tournament entries already running continue. |
POST /api/v1/players/{id}/unblock | Lifts the block. |
POST /api/v1/players/{id}/limits{"maxBuyInPerDay":20000,"maxTables":2,"sessionMinutes":120} | Per-player limits (integers or null to clear). maxBuyInPerDay counts accepted debits since 00:00 UTC (DAILY_LIMIT); maxTables can only lower the operator limit (TABLE_LIMIT); sessionMinutes refuses new buy-ins and registrations once that long has passed since the player's session started (SESSION_LIMIT). The body replaces all previous limits. |
POST /api/v1/players/{id}/kick | Removes the player from every table now (after the current hand if they are in it) and closes their connections. |
Reports and settlement
The back office shows the same data as the API and exports CSV. Amounts are minor units of each row's currency.
| GGR field | Description |
|---|---|
rake | Cash and fast-fold rake attributed to your players (contributed method). |
fees | MTT and Sit & Go fees paid by your players. |
spinMargin | Spin buy-ins minus Spin prizes paid to your players. |
promoCost | Cash Drops paid on your tables. |
ggr | rake + fees + spinMargin − promoCost. |
platformShare | OrbiPoker's revenue share of ggr under your agreement. |
networkSettlement | Your players' net cash loss minus their rake. In network liquidity, a positive value is money your players lost to other operators' players (you owe it to the network); a negative value is owed to you. |
tournamentNet · hands · players · entries | Tournament money in minus money out, and volume counters. |
GET /api/v1/reports/ggr?from&to&groupBy=day|total— one row per day (UTC) and currency. Default range: the last 30 days.GET /api/v1/reports/players?from&to— per player:hands,rake,cashNet,tournamentFees,tournamentEntries. Use it for rakeback, loyalty and affiliate payouts. Default range: the last 7 days.GET /api/v1/transactions?status=— every wallet movement with its delivery status, attempts, last error and the balance you returned. Reconcile it against your wallet ledger bytransactionId.GET /api/v1/handsand/hands/{id}— hand search and full hands (JSON or text) for support and integrity reviews. Integrity alerts (same network, chip dumping, bot-like timing) are listed in the back office.
Sandbox
- /demo opens the game as an anonymous demo player of the built-in demo operator (network liquidity, USD and BRL).
- /sandbox plays the operator: it creates launches for a player id you choose, embeds them with the SDK and shows the events. It mirrors
POST /api/v1/launch. - The demo operator's wallet is a built-in receiver that speaks the exact contract described above over real HTTP, signatures, idempotency, rollbacks and tombstones included. Every demo player starts with 1,000,000 minor units (10,000.00).
- Demo tables are kept busy by practice bots named “Bot…”. Bots only ever join tables, pools and tournaments of sandbox operators; real-money operators never see them.
- Your own sandbox operator (ask OrbiPoker) may use an HTTP wallet URL and any embedding origin, so you can point it at a local tunnel running the reference receiver.
Go-live checklist
- API key stored in your secret manager, used only server-side; a second key ready for rotation.
- Launch created per poker open, never cached;
parentOriginset;sessionTokenpassed if you want to tie movements to sessions. - All production origins (including
wwwand app web views) in the allow-list. - Wallet URL on HTTPS with a public certificate; responds in well under 8 s at peak.
- Signature verified over the raw body with a 300 s tolerance; bad signatures get 401.
- Idempotency by
transactionIdinside the balance transaction; duplicates return the original outcome; body-hash conflicts return 409. - Rollbacks restore the debit once; unknown references create a tombstone; a debit arriving after its tombstone is refused.
- No code path returns non-2xx after a debit was applied.
- Monitoring on
retrying,failedandunknowntransactions; a daily reconciliation of/api/v1/transactionsagainst your ledger. - Responsible-gaming tools wired to block, limits and kick; self-exclusion tested end to end.
- Theme, logo, locales and enabled formats reviewed on a phone; Deposit and Exit buttons wired through the SDK.
- Liquidity choice (network or private), currencies and revenue share agreed with OrbiPoker. RNG certification status checked for your jurisdictions.
AI agents
Using an AI coding agent for the integration? Point it at these plain-text sources; they need no JavaScript or login.
- /llms.txt — index of machine-readable sources.
- /integration-agent.md — step-by-step integration guide with acceptance tests.
- /openapi.json — OpenAPI 3.1 for the operator API and the wallet webhook.
- /wallet-receiver.mjs — reference wallet receiver.
Changelog
- 1.0.0 · 2026-10-19
- First public version: launch API, iframe client and SDK v1, seamless wallet v1 (balance, debit, credit, rollback), operator REST API, cash tables, fast-fold, Sit & Go, Spin & Go, MTT with PKO, back office, provably fair shuffle. Added during the release: Cash Drops (
promotions.cashDrop,promoCost),GET /api/v1/reports/players,inPlayon player profiles and thesessionMinuteslimit.
Wallet payloads carry version. New optional fields may be added at any time; ignore fields you do not know. Breaking changes get a new version and advance notice.