Message Protocol
Kalamba SDK, Kalamba SDK Wrapper and the wrapper plugins communicate exclusively through window.postMessage. Game developers normally never touch raw messages — KalambaSdk methods (configure, openGame, play, send, on) wrap all of it. This page documents the wire contract for wrapper and plugin authors. See Building a Wrapper and Plugins.
Transport
Every message is a plain object posted via window.postMessage:
{
message: `kalamba:${domain}:${name}`, // e.g. 'kalamba:sdk:play'
payload: unknown // message-specific, may be undefined
}
Two channels are used:
- Game ↔ Wrapper (cross-frame) — each side posts to the other frame's
Window, passed in asmessagePort(the wrapper's window from the game's perspective, and vice versa), with target origin'*'. - Wrapper ↔ Plugins (same frame) — the wrapper and its plugins post to their own
windowand listen on it.
When the wrapper forwards a message to plugins it rewrites the prefix to kalamba:wrapper-<targetDomain>:<name> (e.g. kalamba:sdk:openGame arrives at the RGS plugin as kalamba:wrapper-rgs:openGame). This marks the message as already forwarded so the wrapper's own listeners ignore it and it is not forwarded twice. When the wrapper forwards a plugin message to the game, it re-posts it to the game frame as kalamba:wrapper:<name> — from the game's point of view everything arrives from the wrapper domain.
The wrapper's own send() broadcasts each message twice: once on its own window (for plugins) and once to the game frame.
Domains
Each domain has its own payload map in packages/sdk/src/types.ts (SdkOnlyMessagePayloadMap, WrapperOnlyMessagePayloadMap, etc.).
| Domain | Prefix | Sent by | Listened to by |
|---|---|---|---|
sdk | kalamba:sdk:* | Game client (KalambaSdk) | Wrapper; forwarded to casino / RGS / telemetry plugins |
wrapper | kalamba:wrapper:* | Wrapper (KalambaSdkWrapper) | Game client and plugins |
casino | kalamba:casino:* | Casino plugins | Wrapper; forwarded to game client and RGS plugins |
rgs | kalamba:rgs:* | RGS plugin | Wrapper; forwarded to game client, casino and telemetry plugins |
telemetry | kalamba:telemetry:* | Telemetry plugins | Wrapper (payloads untyped) |
Routing
KalambaSdkWrapper.forwardMessages() sets up the fan-out — it is the source of truth for these routes:
| Direction | Forwarded messages |
|---|---|
| sdk → casino plugins | autoplay, bet, cashier, choice, close, configure, error, history, loadEnd, loadProgress, loadStart, openGame, playCycleEnd, playCycleStart, playEnd, playReady, playStart, settings, fullscreen |
| rgs → casino plugins | playResponse, openGameResponse |
| sdk → rgs plugin | history, openGame |
| casino → rgs plugin | play, cashier, close, history, getBalance |
| sdk → telemetry plugins | autoplay, error, loadStart, loadProgress, loadEnd, playCycleStart, playCycleEnd, telemetry.click, telemetry.orientationChange |
| rgs → telemetry plugins | openGameResponse, playResponse |
| casino → sdk (game) | balance, bet, choice, freeze, help, paytable, resume, settings, suspend, unfreeze |
| rgs → sdk (game) | balance, realityCheck, openGameError, openGameResponse, playError, playResponse |
Note that sdk:play is deliberately not forwarded: the wrapper intercepts it and re-emits it as wrapper:play with the injected payload attached (see Lifecycle); plugins consume the wrapper:play broadcast instead.
Plugin-bound messages arrive as kalamba:wrapper-casino:*, kalamba:wrapper-rgs:* or kalamba:wrapper-telemetry:*; game-bound messages arrive as kalamba:wrapper:*.
Lifecycle
The sequence diagram shows the full system, including the RGS and the casino backend. On the wire, a session looks like this:
sdk:configure— the game callssdk.configure(config); the SDK attaches its ownsdkVersionto the payload automatically.- Compatibility check — the wrapper merges the config, compares
sdkVersionagainst its compatibility manifest (semverminSdkcheck) and hands the result to the host'sonCheckcallback. When the host proceeds, the wrapper sendswrapper:wrapperConfigured, which resolves the game'sconfigure()promise. sdk:openGame— the game callssdk.openGame(). The wrapper forwards it to the RGS plugin (kalamba:wrapper-rgs:openGame), which performs the actual RGS call and answers withrgs:openGameResponse(orrgs:openGameError).- Configuration burst — on
rgs:openGameResponsethe wrapper loads the UI config, then sendswrapper:configured(the fullSdkConfig),wrapper:legalBets(bet.available),wrapper:bet(bet.lastPaidfalling back tobet.default) andwrapper:balance. The response itself is also forwarded to the game;sdk.openGame()resolves once both the response andconfiguredhave arrived. From this pointsdk.configandsdk.i18nare available. - Per-spin cycle:
sdk:playCycleStart— the game announces a spin; the wrapper optimistically sendswrapper:balancewith the bet deducted (skipped during free rounds).sdk:play— sent bysdk.play(contract). The wrapper intercepts it, collects extra payload from registered payload injectors (e.g. free round id) and re-emitswrapper:playwithpayloadToInjectattached, which the RGS plugin consumes.rgs:playResponse— forwarded to the game aswrapper:playResponse, resolving theplay()promise.sdk:playStart/sdk:playEnd— the game presents the result. OnplayEndthe wrapper awaits any pending play blockers, then sendswrapper:playReady(the game may spin again).sdk:playCycleEnd— the wrapper sends the settledwrapper:balancefrom the play response and handles free round offers/completion.
Errors and timeouts
The RGS plugin answers every request either with a response or with the matching error message: rgs:openGameError / rgs:playError, carrying { type: 'timeout' } or { type: 'error', data: { code } }. The RGS plugin implementation is expected to enforce ui.requestTimeoutMs from SdkConfig and reject with the timeout variant when the RGS does not respond in time. On the game side openGame() / play() reject with TimeoutError or RgsError respectively; in parallel the wrapper shows its own error popup unless the code is listed in skipErrors. See Error Handling.