Built to be built on.
Typed, documented, versioned.
Contracts
One source of truth.
Generated, never retyped.
The hub interfaces, events and payloads are written once, in C#. A generator reads them and writes the TypeScript every client imports — discriminated unions for events, string enums, camelCase — and the back office does the same from the admin API's OpenAPI document. A changed contract is a compile error in the client, not a surprise in production.
- A player's own hole cards go to that player alone, and a contract test proves no spectator envelope ever carries a face-down card.
- A test compares the committed TypeScript with the generator's output, so the types a client builds against are always the ones the server speaks.
// GENERATED by Poker.Contracts.Generator — do not edit. Regenerate: dotnet run --project contracts/Poker.Contracts.Generator
// Protocol 1.1. Money values are integers in minor units; cards are two-character strings such as "As".
export const PROTOCOL_VERSION = "1.1" as const;
export const PROTOCOL_MAJOR = 1 as const;
export const TABLE_HUB_PATH = "/hubs/table" as const;
export const LOBBY_HUB_PATH = "/hubs/lobby" as const;
export const PROTOCOL_VERSION_HEADER = "X-Poker-Protocol" as const;
// …
export interface RoomEventBatch {
roomId: string;
serverTimeMs: number;
events: RoomEventItem[];
}
The table protocol
Documented for any client.
Two versions, one table.
A client announces the protocol it speaks when it connects; one that announces nothing, or something unknown, gets 1.0 — so old and new apps play at the same table and a store release is never a forced upgrade.
46%
fewer bytes per player
and 75% fewer messages with protocol 1.1, old and new clients at the same table
/hubs/table?access_token=…&X-Poker-Protocol=1.1
interface RoomEventBatch {
roomId: string // every event in the batch is this room's
serverTimeMs: number // server time the burst was produced, Unix ms — use it for clock skew
events: RoomEventItem[] // in order
}
interface RoomEventItem {
seq: number // the room sequence number, exactly as 1.0 numbers it
handId?: string | null // only on HandStarted; every later event belongs to that hand
payload: RoomEventPayload
}
- Room events
- Protocol 1.0One RoomEvent message per event
- Protocol 1.1 · currentOne RoomEvents message per burst — everything one action set off
- Envelope
- Protocol 1.0roomId, seq, handId and serverTimeUtc on every event
- Protocol 1.1 · currentroomId and serverTimeMs once per batch; seq per item; handId only on HandStarted
- Your turn
- Protocol 1.0A private YourTurn message
- Protocol 1.1 · currentBuilt by the client from DecisionRequested and AwaitingAction, in the same batch
- Who gets it
- Protocol 1.0Any client that announces 1.0, nothing, or a version it does not know
- Protocol 1.1 · currentClients that announce 1.1 when they connect
Hubs, methods and events
Two hubs. Every call typed.
Every event in order.
Table hub
/hubs/table
The client calls · 20
- JoinTable
- LeaveTable
- Sit
- StandUp
- SubmitAction
- SitOut
- RequestRebuy
- UseTimeBank
- RequestSnapshot
- SendChat
- Reauthenticate
- JoinWaitlist
- LeaveWaitlist
- ChangeSeat
- InviteUser
- KickSeat
- SetRoomPaused
- CloseRoom
- UpdateRoomConfig
- AnswerRunIt
The server sends · 5
- RoomEvents
- RoomEvent
- YourTurn
- ActionRejected
- RoomSnapshot
Lobby hub
/hubs/lobby
The client calls · 15
- ListRooms
- ListTemplates
- Subscribe
- Unsubscribe
- CreateRoom
- CreateRoomFromTemplate
- ListTournaments
- GetTournament
- RegisterTournament
- UnregisterTournament
- TournamentRebuy
- TournamentAddOn
- ListTickets
- SubscribeTournaments
- UnsubscribeTournaments
The server sends · 3
- RoomSummaryChanged
- RoomRemoved
- TournamentChanged
Sequencing and resync
Sequenced events, snapshot resync
Every room event carries the room's sequence number; a client that sees a gap stops, asks for a snapshot and carries on from it.
- Each item's seq is the previous one plus one; chat, which is not sequenced, carries -1.
- A private event — a player's own hole cards — arrives as the private version for its owner and the hidden one for everyone else, at the same seq.
- After a reconnect, the snapshot carries the player's pending decision, so a turn is never lost.
- Rejected actions come back as ActionRejected with the engine's reason, in their place in the order.
Back-office API
OpenAPI, settled and checked.
A typed client on top.
The admin API serves the same document on every request, and a test compares it with the checked-in copy the back office generates its types from. Staff sign in through your identity provider; every call is checked against the caller's permissions, and every command and sensitive read is audited.
- Rooms
- Players
- Play-chip grants
- Ledger
- Reconciliation
- Dead letters
- Approvals
- Audit
- Settings
- Rake
- Games offered
- Tournaments
- Sit & Go lines
- Social store
- Hands
- History and exports
- Staff and roles
- Live dashboard
- Feature flags
/**
* The typed client. Every shape here comes from `schema.d.ts`, which is generated from the admin API's
* OpenAPI document, so the UI cannot drift from the API — a changed endpoint is a
* compile error here rather than a surprise at runtime.
*/
export type Schemas = components['schemas']
export type MeView = Schemas['MeView']
export type RoomSummary = Schemas['RoomSummary']
export type RoomAdminView = Schemas['RoomAdminView']
Vendor adapters
Bring your own vendors.
The game never notices.
Every outside dependency sits behind an interface the platform owns. Adapters are configured per operator, and each implementation is held to the same contract tests — so a new payment provider or KYC vendor is an adapter, not a fork.
/// <summary>
/// Deposits and withdrawals. The adapter moves money in the outside world; the ledger entry
/// is written by the platform when the provider confirms, never before.
/// </summary>
public interface IPaymentProcessor
{
/// <summary>Which methods and currencies this adapter handles, so the client only offers what works.</summary>
IReadOnlyCollection<PaymentMethod> SupportedMethods { get; }
Task<PaymentResult> Deposit(Guid tenantId, PaymentRequest request);
Task<PaymentResult> Withdraw(Guid tenantId, PaymentRequest request);
/// <summary>Verifies and parses a provider webhook; the platform reconciles it against its own record.</summary>
Task<PaymentResult?> ParseWebhook(Guid tenantId, string payload, IReadOnlyDictionary<string, string> headers);
}
Payments
IPaymentProcessor
Deposits, withdrawals and provider webhooks. The ledger entry is written when the provider confirms, never before.
Identity
IIdentityVerification
Submit a player's identity check and read its status; real-money rooms, deposits and withdrawals gate on it.
Geolocation
IGeolocation
Where a player connects from — country, region, VPN and proxy flags — for jurisdiction gating.
Sanctions
ISanctionsScreening
Sanctions and PEP screening. An empty answer means no match, never “not checked”.
AML
IAmlMonitoring
Transaction monitoring, fed by the platform as money moves, without ever holding up a player.
Self-exclusion
ISelfExclusionRegistry
National self-exclusion registers, checked before a real-money session starts.
Messaging
IEmailSender · ISmsSender · IPushSender
Email, SMS and push notifications, each sent on behalf of the operator they belong to.
Ads
IAdProvider
Rewarded video and interstitials. A reward is paid on the network's server-to-server verification, never on the client's word.
Integrate
Your systems, connected.
01
Partner API and webhooks
02
SDKs
03
The engine API reference
04
Install, operations and integration guides
Read the contracts.
Then watch them run.
A walkthrough for your engineers: the protocol on a live table, the admin API behind the back office, and where your vendors plug in.