orbipoker Docs

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.

B2B only. OrbiPoker never takes players directly. These docs are for operators (casinos, sportsbooks and platforms) and their developers.

Architecture

OrbiPoker architecture The operator backend creates a launch through the OrbiPoker API and embeds the returned URL in an iframe on the operator page. The iframe talks to OrbiPoker over WebSocket. OrbiPoker calls the operator wallet webhook to debit and credit the player balance, and posts events to the host page. YOUR PLATFORM ORBIPOKER Your backend player session · orbp_sk_ key Your wallet webhook balance · debit · credit · rollback Your page + iframe SDK · postMessage events Operator API /api/v1/* · /openapi.json Game engine + wallet outbox tables · tournaments · retries Game client /launch/… · WebSocket /ws 1 · POST /api/v1/launch launchUrl · 120 s 3 · signed wallet calls 2 · iframe src=launchUrl orbipoker:* events
  • 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.

  1. 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.
  2. Run the reference wallet. Download wallet-receiver.mjs (Node 24, no dependencies) and run it with your secret. Expose it over HTTPS.
  3. Configure the back office. In /backoffice set the wallet URL, add the origins that will embed the iframe and press “Test wallet”.
  4. Create a launch and embed it. Call POST /api/v1/launch from your server and mount the returned URL with the SDK.
  5. Play a hand. Sit at a table: your wallet receives a debit. Leave: it receives a credit.
# 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…
StatusCodeMeaning
401UNAUTHORIZEDMissing, malformed, unknown or revoked key.
403TENANT_SUSPENDEDYour operator account is suspended. Player sessions are revoked too.
429RATE_LIMITEDMore 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 }
FieldRuleDescription
playerId required1–128 · [A-Za-z0-9_.@:+-]Your stable player id. It comes back as playerId in every wallet call.
currency requiredenabled for your accountThe player's wallet currency. Tables and tournaments are filtered by it.
nickname3–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.
localept-BR · en · esClient language. Defaults to the player's last locale, then your defaultLocale.
countryISO 3166-1 alpha-2Shown as a flag at the table.
parentOriginmust be in your allow-listThe origin of the page that embeds the iframe. The client only posts events to this origin.
sessionTokenstring ≤ 512Opaque value echoed in every wallet call for this session, so you can tie movements to your own session.
lobbycash · fastfold · sng · spin · mttOpens the lobby on that tab, for example from a “Spin & Go” tile in your casino lobby.
tabletable id ≤ 64Opens a specific table.
themesame schema as config.themePer-session theme override, validated like the operator theme.
depositUrl · exitUrlhttp(s) URLWhere 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-expired and 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 different playerId fails with 409 IDEMPOTENCY_CONFLICT.
  • Origins. parentOrigin must exactly match one of your allow-listed origins (HTTPS, no path; plain HTTP only for localhost), otherwise 403 ORIGIN_NOT_ALLOWED.
  • Framing (CSP). /launch/<code> is served with Content-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_BLOCKED until the block ends.
  • Strict body. Unknown fields are rejected with 400 INVALID_BODY, so typos fail loudly.
StatusCodeMeaning
400INVALID_PLAYER · INVALID_NICKNAME · INVALID_LOCALE · INVALID_COUNTRY · INVALID_URL · INVALID_LOBBY · INVALID_THEME · INVALID_IDEMPOTENCY_KEYA field failed validation.
400INVALID_CURRENCYThe currency is not enabled for your account.
403ORIGIN_NOT_ALLOWED · PLAYER_BLOCKEDSee the rules above.
409IDEMPOTENCY_CONFLICTKey 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 origin and source are that iframe.
  • The iframe height follows orbipoker:resize (320–20,000 px) unless you pass height (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).

typeFieldsWhenSDK
orbipoker:readyplayerId, nickname, currency, locale, tenantIdThe session is authenticated and the lobby is shown.onReady
orbipoker:resizeheightContent height changed.automatic
orbipoker:balancebalance, currencyThe 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:errorcode, messageA fatal client error (for example LAUNCH_EXPIRED).onError
orbipoker:table-openedtableIdThe player opened a table.onEvent
orbipoker:hand-completetableId, handId, netA 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 *.

Mobile apps. In a native app, load the launch URL in a web view whose page is on an allow-listed origin, or host a small page on your domain that mounts the SDK.

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}
FieldTypeDescription
version1Payload version.
eventbalance · debit · credit · rollbackWhat to do.
transactionIdUUIDIdempotency key. Unique per movement and identical on every retry.
referenceTransactionIdUUID | nullFor rollback: the debit being reversed.
tenantIdstringYour operator id. Reject anything that is not yours.
playerIdstringYour player id from the launch.
currencystringThe player's currency.
amountinteger ≥ 0Minor units. Absent on balance.
reasonstringWhy the money moves (table below). Store it for reporting.
tableId · tournamentIdstring | nullWhere the chips are. Fast-fold uses the pool id as tableId.
handIdstring | nullReserved; currently always null.
sessionTokenstring | nullThe sessionToken you passed at launch. It may be null for movements made after the session ended (recovery, refunds, prizes).
createdAtmsWhen 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)

Responses

EventAcceptNon-2xx or timeout
balance2xx with {"balance": integer} (required).The client shows the balance as unavailable. Nothing is retried. Do not store these requests: their transactionId is random.
debit2xx 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.
credit2xx 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.
rollback2xx after restoring the referenced debit, or after recording a tombstone if you never saw it.Retried like a credit.
Never answer non-2xx after moving money. A non-2xx debit tells OrbiPoker the player was not charged, so no chips are created and no rollback is sent. If your handler fails after updating the balance, it must still commit and answer 2xx, or roll its own change back before answering.

Idempotency

  • Deduplicate by transactionId in 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 transactionId with a different body is a bug or an attack: answer 409 and 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.

  1. Debit applied: restore exactly the debited amount once, mark the debit as rolled back, answer 2xx.
  2. Debit never seen (the rollback overtook it, or it never arrived): store a tombstone for referenceTransactionId and answer 2xx without moving money.
  3. Debit arrives after its tombstone: refuse it with a non-2xx status (for example 409). Money must not move.
  4. 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 failed and shows in the back office for manual reconciliation (POST /api/v1/transactions/{id}/retry resends 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).
StatusDescription
pendingRecorded, first attempt in flight.
okYou answered 2xx.
declinedDebit refused with a non-2xx status.
unknownDebit timed out; a rollback was queued.
retryingCredit or rollback waiting for its next attempt.
failedGave up after 24 h. Reconcile manually.

When money moves

reasonEventWhen
Cash tables
cash_buyindebitThe player sits down (or is seated from the wait list) with the chosen buy-in.
cash_topupdebitManual or automatic top-up. Chips added mid-hand play from the next hand.
cash_cashoutcreditThe 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_closedcreditYou or OrbiPoker closed the table; stacks are returned after the current hand.
cash_recoverycreditAfter a server restart: the stack at the last completed hand is returned and the interrupted hand is void.
Fast-fold
fastfold_buyin · fastfold_topupdebitJoining a pool, or topping up between hands.
fastfold_cashoutcreditLeaving the pool, removal after sitting out, or pool closed.
Sit & Go, Spin & Go and MTT
tourney_buyindebitRegistration or re-entry: buy-in + fee + bounty in one debit.
tourney_refundcreditUnregistering before the start, or a tournament cancelled before it starts (including an MTT that misses its minimum players).
tourney_prizecreditFinishing 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_bountycreditPKO: the cash half of a bounty, sent immediately after the knockout.
tourney_recoverycreditAfter a server restart during a running tournament: the remaining prize pool split by chip count plus each player's current bounty.
Other
debit_timeoutrollbackA 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.

MethodPathSummary
Loading /openapi.json…

Notes

  • GET /api/v1/players/{playerId} accepts your playerId and returns status, limits, lifetime stats, recent hands, tournaments, wallet movements and inPlay (chips currently on tables and active tournament entries).
  • POST /api/v1/tables creates a private table for your players, or a shared one with "network": true if your liquidity is network. Fields: name (2–40), kind (cash | fastfold), variant, currency, bb, optional sb (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) and features (straddle, runItTwice, rabbitHunt, anonymous).
  • POST /api/v1/tournaments creates an MTT (one-off unless template: true with recurringMinutes) or a Sit & Go / Spin template that keeps one instance open at all times. See Games and rules for defaults. DELETE cancels a tournament that has not started (everyone is refunded) or archives a template (tt_…).
  • GET /api/v1/hands/{handId}?format=text returns 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 } }
KeyDefaultAllowedDescription
brand
brand.nameoperator name2–40Name shown in the client.
brand.logoUrlnullHTTPS URLLogo in the lobby header.
brand.lobbyTitle"Poker"stringLobby title.
theme
theme.preset"midnight"midnight · classic · royal · lightSending a preset without colors loads that preset's palette.
theme.colors.*midnight palette#rrggbb15 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-uiClient font.
theme.radius120–24Corner radius in px.
theme.deck"four-color"four-color · two-colorSuit colours.
theme.cardFace"classic"classic · large-indexCard face style; large index reads better on phones.
theme.tableShape"oval"oval · roundedTable shape.
theme.animations · theme.soundstruebooleanDefaults for new players; each player can change them.
features
features.cash · fastFold · sitAndGo · spins · mtttruebooleanFormats your players can join (enforced by the server).
features.chat · emotes · notes · hudtruebooleanChat, emotes, player notes and the built-in HUD (enforced by the server).
features.straddle · runItTwice · rabbitHunttruebooleanAllowed on your tables; each table must also enable it.
features.allInEquity · handHistory · replayer · provablyFair · waitlist · autoTopUp · showLeaderboards · multiTabletruebooleanClient features shown to players.
features.anonymousTablesfalsebooleanDefault for anonymous tables in the client. Anonymity itself is a per-table flag.
timers
timers.actionSeconds155–60Time to act at cash tables.
timers.fastFoldActionSeconds105–30Time to act in fast-fold.
timers.tournamentActionSeconds125–60Time to act in tournaments.
timers.timeBankSeconds150–300Starting time bank; used automatically when the action clock runs out.
timers.timeBankMaxSeconds600–600Maximum time bank.
timers.timeBankRefillSeconds50–60Seconds added every timeBankRefillHands hands.
timers.timeBankRefillHands101–1000Hands per refill.
timers.sitOutKickMinutes101–120A player sitting out longer than this is cashed out and leaves the table.
timers.handPauseMs2500500–10000Pause between hands at cash tables.
timers.missedBlindsSitOut21–10Reserved; not used yet.
limits · betting
limits.maxTables41–12Cash tables plus fast-fold pools a player can play at once. A per-player limit can only lower it.
limits.maxBuyInPerDaynullminor unitsDefault daily buy-in cap for every player (UTC day). A per-player limit replaces it.
limits.sessionReminderMinutes600–1440Reality-check reminder shown by the client.
betting.preflopRaises[2.2, 2.5, 3, 4]≤ 6 values, 1–20 BBPreflop raise buttons, in big blinds.
betting.postflopPotPercents[33, 50, 75, 100]≤ 6 integers, 10–300Postflop bet buttons, in % of the pot.
betting.showPotButtontruebooleanShow a Pot button.
rake · fees · promotions
rake.bps5000–1000Cash rake in basis points (500 = 5%). A table's rakeBps overrides it.
rake.noFlopNoDroptruebooleanNo rake when the hand ends before the flop.
rake.capsByPlayers{"2":100,"4":200,"10":300}integer keys and valuesCap 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.capUnitBbtruebooleanCaps in BB/100 (true) or minor units (false). A table's rakeCap is always an absolute amount.
tournamentFeeBps10000–2500Default MTT and Sit & Go fee as a share of the buy-in, when a tournament does not set fee.
spinFeeBps700700 | 800Spin 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 ≤ 500Cash 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 subsetLanguages offered in the client.
defaultLocale"pt-BR"one of localesLanguage for new players without a locale.
chat.maxLength · chat.rateSeconds120 · 210–500 · 0–60Message length and one message per N seconds.
chat.bannedWords[]≤ 500 words, 1–40 charsWords 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

CodeGameNotes
NLHENo-Limit Hold'em2 hole cards.
PLHEPot-Limit Hold'emPot-limit betting.
PLO4 · PLO5 · PLO6Pot-Limit Omaha4, 5 or 6 hole cards; exactly two from hand and three from the board. PLO5/PLO6 up to 6 seats.
PLO8Omaha Hi/LoEight-or-better low; pots split high/low, odd chip to the high.
SHORTShort 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 timeBankRefillSeconds every timeBankRefillHands hands up to timeBankMaxSeconds.
  • 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 sitOutKickMinutes cashes 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 & GoSpin & GoMTT
Seats2–9 (6)32–9 (8) per table
Start stack / BB1,500 / 20500 / 2010,000 / 100
Speed (level)turbo (5 min)2–5 min by multiplierregular (10 min)
BB ante from level4never3
FeetournamentFeeBpsin the multiplier tabletournamentFeeBps
Startswhen fullwhen fullstartAt
  • 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 lateRegLevels levels (default 8), closing early if 15% or fewer entrants remain. Re-entry up to maxEntries (1–10) while late registration is open. If fewer than minPlayers are registered at startAt, 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 (set progressiveBounty: false for 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).

MultiplierPer 1,000,000 (7%)Per 1,000,000 (8%)Prizes (1st / 2nd / 3rd)Level
12,000×1183.3 / 8.3 / 8.3%5 min
240×303083.3 / 8.3 / 8.3%5 min
120×757583.3 / 8.3 / 8.3%5 min
25×1,0001,00080 / 10 / 10%4 min
10×5,0005,00080 / 20%4 min
5×85,00085,000100%3 min
3×444,000414,012100%3 min
2×464,894494,882100%2 min

Provably fair shuffle

  1. Before each hand the server draws a 32-byte serverSeed from the operating system's CSPRNG and publishes commitment = SHA-256(serverSeed bytes) in the table view.
  2. 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 of HMAC-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.
  3. 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).
  4. When the hand ends, serverSeed is 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}.

Try it: rebuild a deck

Responsible gaming

You own the player relationship, so you drive these controls from your own tools. All of them are audited.

CallDescription
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}/unblockLifts 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}/kickRemoves 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 fieldDescription
rakeCash and fast-fold rake attributed to your players (contributed method).
feesMTT and Sit & Go fees paid by your players.
spinMarginSpin buy-ins minus Spin prizes paid to your players.
promoCostCash Drops paid on your tables.
ggrrake + fees + spinMargin − promoCost.
platformShareOrbiPoker's revenue share of ggr under your agreement.
networkSettlementYour 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 · entriesTournament 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 by transactionId.
  • GET /api/v1/hands and /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; parentOrigin set; sessionToken passed if you want to tie movements to sessions.
  • All production origins (including www and 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 transactionId inside 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, failed and unknown transactions; a daily reconciliation of /api/v1/transactions against 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.

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, inPlay on player profiles and the sessionMinutes limit.

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.