Skip to main content

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 as messagePort (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 window and 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.).

DomainPrefixSent byListened to by
sdkkalamba:sdk:*Game client (KalambaSdk)Wrapper; forwarded to casino / RGS / telemetry plugins
wrapperkalamba:wrapper:*Wrapper (KalambaSdkWrapper)Game client and plugins
casinokalamba:casino:*Casino pluginsWrapper; forwarded to game client and RGS plugins
rgskalamba:rgs:*RGS pluginWrapper; forwarded to game client, casino and telemetry plugins
telemetrykalamba:telemetry:*Telemetry pluginsWrapper (payloads untyped)

Routing

KalambaSdkWrapper.forwardMessages() sets up the fan-out — it is the source of truth for these routes:

DirectionForwarded messages
sdk → casino pluginsautoplay, bet, cashier, choice, close, configure, error, history, loadEnd, loadProgress, loadStart, openGame, playCycleEnd, playCycleStart, playEnd, playReady, playStart, settings, fullscreen
rgs → casino pluginsplayResponse, openGameResponse
sdk → rgs pluginhistory, openGame
casino → rgs pluginplay, cashier, close, history, getBalance
sdk → telemetry pluginsautoplay, error, loadStart, loadProgress, loadEnd, playCycleStart, playCycleEnd, telemetry.click, telemetry.orientationChange
rgs → telemetry pluginsopenGameResponse, 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:

  1. sdk:configure — the game calls sdk.configure(config); the SDK attaches its own sdkVersion to the payload automatically.
  2. Compatibility check — the wrapper merges the config, compares sdkVersion against its compatibility manifest (semver minSdk check) and hands the result to the host's onCheck callback. When the host proceeds, the wrapper sends wrapper:wrapperConfigured, which resolves the game's configure() promise.
  3. sdk:openGame — the game calls sdk.openGame(). The wrapper forwards it to the RGS plugin (kalamba:wrapper-rgs:openGame), which performs the actual RGS call and answers with rgs:openGameResponse (or rgs:openGameError).
  4. Configuration burst — on rgs:openGameResponse the wrapper loads the UI config, then sends wrapper:configured (the full SdkConfig), wrapper:legalBets (bet.available), wrapper:bet (bet.lastPaid falling back to bet.default) and wrapper:balance. The response itself is also forwarded to the game; sdk.openGame() resolves once both the response and configured have arrived. From this point sdk.config and sdk.i18n are available.
  5. Per-spin cycle:
    1. sdk:playCycleStart — the game announces a spin; the wrapper optimistically sends wrapper:balance with the bet deducted (skipped during free rounds).
    2. sdk:play — sent by sdk.play(contract). The wrapper intercepts it, collects extra payload from registered payload injectors (e.g. free round id) and re-emits wrapper:play with payloadToInject attached, which the RGS plugin consumes.
    3. rgs:playResponse — forwarded to the game as wrapper:playResponse, resolving the play() promise.
    4. sdk:playStart / sdk:playEnd — the game presents the result. On playEnd the wrapper awaits any pending play blockers, then sends wrapper:playReady (the game may spin again).
    5. sdk:playCycleEnd — the wrapper sends the settled wrapper:balance from 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.