Files
jackboxpartypack-gamepicker/docs/jackbox-ecast-api.md
cottongin a7bd0650eb docs: update ecast API reference with connection roles and shard details
Add Connection Roles table documenting host, player, shard, audience,
observer, and moderator roles with their capabilities and slot impact.
Add shard client/welcome capture and passive room monitoring section.

Made-with: Cursor
2026-03-20 11:47:47 -04:00

38 KiB

Jackbox Ecast API Reference

Reverse-engineered documentation of the Jackbox Games ecast platform API. This covers the REST API at ecast.jackboxgames.com and the WebSocket protocol used by jackbox.tv clients to participate in game rooms.

Last updated: 2026-03-20 Tested with: Drawful 2 (appTag: drawful2international)


Table of Contents

  1. Overview
  2. REST API Reference
  3. WebSocket Protocol Reference
  4. Entity Model
  5. Game Lifecycle
  6. Player & Room Management
  7. Passive Room Monitoring
  8. Appendix: Raw Captures

Overview

Jackbox Games uses a platform called ecast to manage game rooms and real-time communication between the game host (running on a console/PC) and players (connecting via jackbox.tv in a browser).

Architecture

┌──────────────┐     HTTPS/WSS      ┌──────────────────────────┐     WSS      ┌──────────────┐
│  Game Host   │◄───────────────────►│  ecast.jackboxgames.com  │◄────────────►│   Players    │
│  (Console)   │                     │  (load balancer)         │              │  (Browser)   │
└──────────────┘                     └──────────┬───────────────┘              └──────────────┘
                                                │
                                     ┌──────────▼───────────────┐
                                     │  ecast-prod-{region}.    │
                                     │  jackboxgames.com        │
                                     │  (game server)           │
                                     └──────────────────────────┘
  • Load balancer (ecast.jackboxgames.com): Routes REST requests and resolves room codes to specific game servers.
  • Game server (e.g., ecast-prod-use2.jackboxgames.com): Hosts WebSocket connections and manages room state. Returned in the host / audienceHost fields of room info.
  • Room code: 4-letter alphanumeric code (e.g., LSBN) that identifies an active game session.

Base URLs

Purpose URL
REST API (rooms) https://ecast.jackboxgames.com/api/v2/
WebSocket (player) wss://{host}/api/v2/rooms/{code}/play
WebSocket (audience) wss://{host}/api/v2/audience/{code}/play

The {host} value comes from the REST API room info response (host or audienceHost field).

Connection Roles

Ecast supports several connection roles, each with different capabilities and visibility:

Role Endpoint here visibility Entity reads Entity writes Slot impact
host /rooms/{code}/play?role=host Full Full Full Yes (always 1)
player /rooms/{code}/play?role=player Full Own + room Own entities Yes (occupies a maxPlayers slot)
shard /rooms/{code}/play?role=shard Full (sees & is seen) Full (via object/get) Denied No (does NOT count toward full / maxPlayers)
audience /audience/{code}/play None (here: null) Receives broadcasts only None No (does NOT count toward full / maxPlayers)
observer /rooms/{code}/play?role=observer Unknown Unknown Unknown Unknown
moderator /rooms/{code}/play?role=moderator Unknown Unknown Unknown Unknown

Notes:

  • observer: Exists but is password-protected ("observer is for internal use, sorry: password required" — error 2017). Presumably designed for exactly the monitoring use case, but the password is not publicly known.
  • moderator: Exists but requires moderationEnabled: true on the room (error 2023: "moderation is not enabled").
  • spectator, empty string, and other arbitrary roles return error 2014: "no such role".
  • Both shard and audience connections increment the raw /connections counter and hold their slot after disconnect (same persistence model as players). However, they do not count toward the full flag or maxPlayers limit — only player role connections do.

REST API Reference

All REST endpoints return JSON. Successful responses have {"ok": true, "body": ...}. Errors have {"ok": false, "error": "..."}.

Response Headers

Header Value Notes
access-control-allow-origin https://jackbox.tv CORS restricted to jackbox.tv
access-control-allow-credentials true
ecast-room-status valid Custom header on room endpoints

GET /api/v2

Health check endpoint.

Response: {"ok": true, "body": "hello"}

GET /api/v2/rooms/{code}

Full room information. This is the primary endpoint for checking room status.

Response:

{
  "ok": true,
  "body": {
    "appId": "fdac94cc-cade-41ff-b4aa-e52e29418e8a",
    "appTag": "drawful2international",
    "audienceEnabled": true,
    "code": "LSBN",
    "host": "ecast-prod-use2.jackboxgames.com",
    "audienceHost": "ecast-prod-use2.jackboxgames.com",
    "locked": false,
    "full": false,
    "maxPlayers": 8,
    "minPlayers": 0,
    "moderationEnabled": false,
    "passwordRequired": false,
    "twitchLocked": false,
    "locale": "en",
    "keepalive": false,
    "controllerBranch": ""
  }
}
Field Type Description
appId string (UUID) Unique application identifier
appTag string Game identifier slug
audienceEnabled boolean Whether audience mode is enabled
code string 4-letter room code
host string Game server hostname for player WebSocket connections
audienceHost string Game server hostname for audience WebSocket connections
locked boolean Whether the room is locked (game started, no new players)
full boolean Whether the room has reached max players
maxPlayers number Maximum number of players allowed
minPlayers number Minimum players required to start
moderationEnabled boolean Whether content moderation is active
passwordRequired boolean Whether a password is needed to join
twitchLocked boolean Whether the room is Twitch-restricted
locale string Language locale
keepalive boolean Whether the room persists after host disconnects
controllerBranch string Controller code branch (empty for production)

Error (room not found): {"ok": false, "error": "no such room"} (HTTP 404)

GET /api/v2/rooms/{code}/status

Lightweight existence check. Returns minimal data.

Response: {"ok": true, "body": {"code": "LSBN"}}

GET /api/v2/rooms/{code}/info

Room information with audience data and join role. Uses a different response format (no ok/body wrapper).

Response:

{
  "roomid": "LSBN",
  "server": "ecast-prod-use2.jackboxgames.com",
  "apptag": "drawful2international",
  "appid": "fdac94cc-cade-41ff-b4aa-e52e29418e8a",
  "numAudience": 0,
  "audienceEnabled": true,
  "joinAs": "player",
  "requiresPassword": false
}
Field Type Description
numAudience number Current audience member count
joinAs string Default role for new connections ("player")
requiresPassword boolean Whether password is needed

Note: numAudience may not update in real-time for WebSocket-connected audience members. Consider using the WebSocket room/get-audience opcode for live counts.

GET /api/v2/rooms/{code}/connections

Total occupied slot count across all connection types: host (1) + players + audience + shards.

Response: {"ok": true, "body": {"connections": 3}}

For rooms with only the host and players (the typical case), player count = connections - 1. Since Jackbox holds player slots for reconnection (a disconnected player's slot is reserved and unavailable to new players), this directly reflects how many of the room's maxPlayers slots are taken. Available slots = maxPlayers - (connections - 1).

Important: This counter includes ALL connection types (audience, shard, etc.), not just players. If external monitoring connections are present, subtract them accordingly. Alternatively, use the full field from GET /rooms/{code} — this flag only considers player-role connections and is unaffected by audience/shard connections.

POST /api/v2/rooms

Create a new room. Returns a room code and management token.

Request:

{
  "appTag": "drawful2international",
  "userId": "your-user-id"
}
Field Type Required Description
appTag string Yes Game identifier
userId string Yes Creator's user ID

Response:

{
  "ok": true,
  "body": {
    "host": "ecast-prod-use2.jackboxgames.com",
    "code": "XCMG",
    "token": "ec356450735cf7d23d42c127"
  }
}

The token is required for subsequent PUT and DELETE operations.

Errors:

  • "invalid parameters: missing required field appTag" (HTTP 400)
  • "invalid parameters: missing required field userId" (HTTP 400)

PUT /api/v2/rooms/{code}?token={token}

Update room properties. Requires the room token from creation as a query parameter.

Request: JSON body with properties to update (e.g., {"locked": true})

Response: {"ok": true} (HTTP 200)

Errors:

  • "missing room token" (HTTP 400) — token not provided or wrong format
  • "bad token" (HTTP 403) — invalid token

Note: Token must be passed as a query parameter (?token=...), not in the request body, headers, or as a Bearer token.

DELETE /api/v2/rooms/{code}?token={token}

Delete/close a room. Requires the room token as a query parameter.

Response: ok (plain text, HTTP 200)

Errors:

  • "no such room" (HTTP 404)
  • "bad token" (HTTP 403)

API Version Notes

Only /api/v2/ is active. Requests to /api/v1/, /api/v3/, or /api/v4/ return 403 Forbidden. The root path / and /api/ also return 403.


WebSocket Protocol Reference

Connection

Player connection URL (new join):

wss://{host}/api/v2/rooms/{code}/play?role=player&name={name}&userId={userId}&format=json
Parameter Required Description
role Yes player
name Yes Display name (URL-encoded)
userId Yes Unique user identifier
format Yes json (only known format)

Player reconnection URL (existing session):

wss://{host}/api/v2/rooms/{code}/play?role=player&name={name}&format=json&secret={secret}&id={id}
Parameter Required Description
secret Yes Session secret from original client/welcome response
id Yes Session ID from original client/welcome response
name Yes Display name (can differ from original)
format Yes json

On reconnection, the server returns client/welcome with reconnect: true and the same id and secret. The player resumes their existing slot rather than consuming a new one.

Audience connection URL:

wss://{host}/api/v2/audience/{code}/play

No query parameters required for audience.

Shard connection URL:

wss://{host}/api/v2/rooms/{code}/play?role=shard&name={name}&userId={userId}&format=json

Shard connections use the same /rooms/{code}/play endpoint as players but with role=shard. They get full here visibility and can query entities but cannot write.

Required WebSocket sub-protocol: ecast-v0

Required headers:

Header Value
Origin https://jackbox.tv
Sec-WebSocket-Protocol ecast-v0

Example (Node.js with ws):

const ws = new WebSocket(url, ['ecast-v0'], {
  headers: { 'Origin': 'https://jackbox.tv' }
});

Message Format

Client → Server:

{
  "seq": 1,
  "opcode": "object/get",
  "params": {
    "key": "room"
  }
}
Field Type Description
seq number Client-side sequence number (monotonically increasing)
opcode string Operation to perform
params object Operation-specific parameters

Server → Client:

{
  "pc": 564,
  "opcode": "client/welcome",
  "result": { ... }
}
Field Type Description
pc number Server-side packet counter (monotonically increasing, shared across all clients in the room)
opcode string Message type
result object Operation-specific payload

Opcode Catalog

Connection Lifecycle (Server → Client)

Opcode Direction Description
client/welcome S→C Sent immediately after connection. Contains initial state, entities, connected users.
client/connected S→C Notification that another client connected (see note below).
client/disconnected S→C Notification that another client disconnected (see note below).
client/kicked S→C Notification that you were kicked from the room.
ok S→C Generic success response to a client request.
error S→C Error response. Contains code and msg.

Important: client/connected and client/disconnected are NOT delivered to player connections. Extensive testing across multiple rooms confirmed that these opcodes never fire for players joining, leaving, or reconnecting — even with graceful WebSocket close. They exist in the client JavaScript and may be delivered to the host connection only, but this could not be verified. Players learn about joins exclusively through textDescriptions entity updates ("X joined."). There is no notification mechanism for player disconnection — Jackbox's design holds player slots open for reconnection indefinitely, and the concept of "leaving" does not exist in the UI or protocol.

Entity Operations (Client → Server)

Object entities (JSON key-value stores):

Opcode Description
object/create Create a new object entity
object/get Read an object entity's value
object/set Replace an object entity's value (host-only for game entities)
object/update Partially update an object entity
object/echo Echo an object to all clients

Text entities (string values):

Opcode Description
text/create Create a new text entity
text/get Read a text entity
text/set Set a text entity's value
text/update Update a text entity
text/echo Echo text to all clients

Number entities (numeric counters):

Opcode Description
number/create Create a new number entity
number/get Read a number entity
number/increment Increment a number
number/decrement Decrement a number
number/update Set a number's value

Stack entities (ordered collections):

Opcode Description
stack/create Create a new stack
stack/push Push an element
stack/bulkpush Push multiple elements
stack/pop Pop the top element
stack/peek Read the top element without removing
stack/element Get a specific element
stack/elements Get all elements

Doodle entities (drawing data):

Opcode Description
doodle/create Create a new doodle
doodle/get Get doodle data
doodle/line Add a line to a doodle
doodle/stroke Add a stroke to a doodle
doodle/undo Undo the last stroke

Room Operations (Client → Server)

Opcode Description Permissions
room/get-audience Get audience connection count Any client
room/lock Lock the room (prevent new joins) Host only
room/exit Close/destroy the room Host only
room/migrate Migrate room to another server Host only
room/set-password Set or change room password Host only
room/start-audience Enable audience connections Host only

room/get-audience response: {"connections": 1}

Client Communication (Client → Server)

Opcode Description
client/send Send a message to a specific client by ID
client/kick Kick a client from the room
error/observed Acknowledge an error was observed

client/send params: {"to": <clientId>, "body": {...}}

Audience Entity Types

Type Description
audience/pn-counter Positive-negative counter (tracks audience count)
audience/g-counter Grow-only counter
audience/count-group Grouped count (for audience voting categories)
audience/text-ring Ring buffer of text entries

Error Codes

Code Message Description
2000 missing Sec-WebSocket-Protocol header WebSocket connection missing required protocol
2003 invalid opcode Unrecognized opcode sent (or opcode not available for this role)
2007 entity value is not of type {expected} Type mismatch (e.g., using text/get on an object entity)
2014 no such role: {role} Requested role does not exist
2017 observer is for internal use, sorry: password required Observer role requires a password
2023 permission denied Operation not allowed for this client's role (also: moderation is not enabled, only the host can close the room)
2027 the room has already been closed Room ended but REST API may still return room info (REST/WS state divergence)

Entity Model

Ecast uses a typed entity system. Each entity has a key, a type, a val (value), a version counter, and a locked flag.

Entity Structure in client/welcome

Entities in the welcome message are arrays with three elements:

["object", {"key": "room", "val": {...}, "version": 0, "from": 1}, {"locked": false}]
  1. Type identifier (string): "object", "audience/pn-counter", etc.
  2. Entity data (object): key, val, version, from (originating client ID)
  3. Metadata (object): locked flag

Entity Updates (object opcode)

When an entity changes, all subscribed clients receive an object message:

{
  "pc": 569,
  "opcode": "object",
  "result": {
    "key": "room",
    "val": { ... },
    "version": 2,
    "from": 1
  }
}

The version increments with each update. The from field indicates which client made the change (1 = host).

Core Entities

room (type: object)

The primary game state entity. Managed by the host.

{
  "key": "room",
  "val": {
    "analytics": [{"appid": "drawful2-nx", "appname": "Drawful2", "appversion": "0.0.0", "screen": "drawful2-lobby"}],
    "audience": {"playerInfo": {"username": "audience"}, "state": "Logo"},
    "gameCanStart": true,
    "gameFinished": false,
    "gameIsStarting": false,
    "lobbyState": "CanStart",
    "locale": "en",
    "platformId": "NX",
    "state": "Lobby",
    "strings": { ... }
  }
}
Field Type Description
state string Current game phase: "Lobby", "Gameplay", etc.
lobbyState string Lobby sub-state: "WaitingForMore", "CanStart", "Countdown", etc.
gameCanStart boolean Whether minimum player count is met
gameFinished boolean Whether the game has ended
gameIsStarting boolean Whether the start countdown is active
analytics array Game tracking metadata (appid, screen name)
platformId string Host platform ("NX" = Nintendo Switch, "steam", etc.)
strings object Localized UI strings

player:{id} (type: object)

Per-player state. Created when a player joins. {id} matches the player's session ID from client/welcome.

{
  "key": "player:2",
  "val": {
    "playerName": "probe1",
    "playerIsVIP": true,
    "playerCanStartGame": true,
    "playerIndex": 0,
    "state": "Draw",
    "colors": ["#fb405a", "#7a2259"],
    "classes": ["Player0"],
    "playerInfo": {
      "username": "PROBE1",
      "sessionId": 2,
      "playerIndex": 0
    },
    "prompt": {"html": "<div>please draw:</div> <div>a picture of yourself</div>"},
    "size": {"height": 320, "width": 240},
    "sketchOptions": { ... }
  }
}
Field Type Description
playerName string Lowercase display name
playerIsVIP boolean Whether this player is the VIP (first to join)
playerCanStartGame boolean Whether this player can trigger game start
playerIndex number Zero-based player order
state string Player's current state (game-specific)
colors array Assigned player colors
playerInfo.username string Original-case display name
playerInfo.sessionId number WebSocket session ID

audience (type: audience/pn-counter)

Tracks the audience member count.

{
  "key": "audience",
  "count": 0
}

textDescriptions (type: object)

Human-readable event descriptions (join/leave notifications).

{
  "key": "textDescriptions",
  "val": {
    "data": {"TEXT_DESCRIPTION_PLAYER_VIP": "PROBE1"},
    "latestDescriptions": [
      {"category": "TEXT_DESCRIPTION_PLAYER_JOINED_VIP", "id": 1, "text": "PROBE1 joined and is the VIP."},
      {"category": "TEXT_DESCRIPTION_PLAYER_JOINED", "id": 2, "text": "PROBE3 joined."}
    ]
  }
}

Categories observed:

  • TEXT_DESCRIPTION_PLAYER_JOINED_VIP — First player joined (becomes VIP)
  • TEXT_DESCRIPTION_PLAYER_JOINED — Subsequent player joined

audiencePlayer (type: object)

Audience-specific state entity. Only sent to audience connections.

{
  "key": "audiencePlayer",
  "val": {
    "analytics": [],
    "audience": {"playerInfo": {"username": "audience"}, "state": "Logo"},
    "locale": "en",
    "platformId": "NX"
  }
}

Game Lifecycle

Connection and Lobby

Client                              Server
  │                                    │
  │──── WebSocket connect ────────────►│
  │                                    │
  │◄─── client/welcome ───────────────│  (id, secret, entities, here, profile)
  │◄─── object: textDescriptions ─────│  ("X joined")
  │◄─── object: player:{id} (v0) ─────│  (empty initial entity)
  │◄─── object: player:{id} (v1) ─────│  (full player state)
  │◄─── object: room ─────────────────│  (lobbyState updated)
  │                                    │

Player Join (from existing player's perspective)

When a new player joins, existing players receive:

  1. object update on textDescriptions with "X joined." in latestDescriptions
  2. object update on room (e.g., lobbyState changes from "WaitingForMore" to "CanStart")

There is no dedicated client/connected message sent to players — join detection relies on entity updates.

Game Start

The room entity transitions through these states:

  1. lobbyState: "WaitingForMore" → Fewer than minimum players
  2. lobbyState: "CanStart" / gameCanStart: true → Minimum players met, VIP can start
  3. gameIsStarting: true / lobbyState: "Countdown" → Start countdown active
  4. state: "Gameplay" → Game has started
  5. REST: locked: true → Room is locked, no new players can join

Game End

  1. room.gameFinished: true → Game is complete
  2. room.state returns to "Lobby" (for "same players" / "new players" options)
  3. Player entities may contain history and result data

Reconnection

Players can reconnect to their existing session using the secret and id from their original client/welcome message. There are two reconnection methods:

Method 1 — Via secret and id query params (programmatic):

wss://{host}/api/v2/rooms/{code}/play?role=player&name={name}&format=json&secret={secret}&id={id}

Method 2 — Via deviceId (browser-based):

If the browser retains the same deviceId (stored in cookies/localStorage), the server automatically matches the reconnecting client to their previous session. The jackbox.tv UI shows a "RECONNECT" button instead of "PLAY" when it detects an existing session.

In both cases, the server responds with client/welcome containing:

  • reconnect: true
  • Same id and secret as the original session
  • Full current state of all entities (complete snapshot, not deltas)

Disconnection and Slot Persistence

Jackbox has no concept of "leaving." When a player's WebSocket closes (gracefully or otherwise), their player slot is held open for reconnection. From the game's perspective, the player is still "in the game" and can return at any time. This means a disconnected player's slot is not available to a new player — it remains reserved.

Key behaviors:

  • The connections REST endpoint count does not decrease when a player's WebSocket closes.
  • The here field in client/welcome continues to list all registered players (connected or not).
  • No client/disconnected event is sent to other players.
  • No textDescriptions update is generated for disconnections.
  • The player's player:{id} entity remains intact with its full state.

Player slots are freed only when the room itself is destroyed (host closes the game) or the room times out.

This slot-persistence model makes player counting straightforward: the number of occupied slots (connections - 1, or count of player roles in here) directly represents how many of the room's maxPlayers slots are taken, regardless of whether those players currently have active WebSocket connections.

REST/WebSocket state divergence: A room can be reported as existing by the REST API (GET /rooms/{code} returns 200) while the WebSocket layer considers it ended (error code 2027). Always verify with a WebSocket connection attempt if the REST response seems stale.


Player & Room Management

Answers to common questions about managing players and rooms.

How to detect when players join

  • WebSocket (best): Watch for textDescriptions entity updates with category: "TEXT_DESCRIPTION_PLAYER_JOINED" or "TEXT_DESCRIPTION_PLAYER_JOINED_VIP".
  • WebSocket (snapshot): The here field in client/welcome lists all registered players (includes both active and disconnected — since slots persist, this reflects the true occupied slot count).
  • REST (polling): GET /api/v2/rooms/{code}/connections — count increases when a new player joins. Does not decrease when they disconnect (by design — the slot is reserved).

There is no "leave" event. Jackbox holds player slots for reconnection, so a player who closes their browser still occupies a slot and can return. The client/disconnected opcode exists in the client code but is not delivered to player connections.

How to count players

Since Jackbox holds player slots for reconnection, the number of occupied slots is the effective player count — a held slot is unavailable to a new player.

Method 1 — REST /connections (recommended for polling):

GET /api/v2/rooms/{code}/connections → {"connections": N}

Player count = N - 1 (subtract host). If audience members are present, subtract audience count too (get via room/get-audience opcode or /info endpoint's numAudience).

Available slots = maxPlayers - (N - 1)

Method 2 — WebSocket here field (recommended for WebSocket clients):

The client/welcome message includes a here object mapping session IDs to roles:

{
  "1": {"id": 1, "roles": {"host": {}}},
  "2": {"id": 2, "roles": {"player": {"name": "P1"}}},
  "3": {"id": 3, "roles": {"player": {"name": "P2"}}},
  "4": {"id": 4, "roles": {"player": {"name": "P3"}}}
}

Count entries where roles contains player. The here field excludes the connecting client itself (you don't see yourself in the list), so add 1 if your own connection is also a player.

Method 3 — WebSocket room/get-audience (audience count only):

Send {seq: N, opcode: "room/get-audience", params: {}} → response {"connections": M}

How to see maximum players allowed

REST: GET /api/v2/rooms/{code}body.maxPlayers (e.g., 8 for Drawful 2)

Also available: body.minPlayers (e.g., 0)

How to detect when the game starts or lobby is locked

REST (polling): GET /api/v2/rooms/{code}body.locked changes from false to true

WebSocket (real-time): Watch room entity updates for:

  • lobbyState transitions: "WaitingForMore""CanStart""Countdown" → (game starts)
  • gameCanStart: true → minimum player count met
  • gameIsStarting: true → start countdown active
  • state changes from "Lobby" to "Gameplay"

How to detect when the game completes

WebSocket: Watch room entity for gameFinished: true

REST: The room will either remain (if "same players" is chosen) or disappear (404 on room endpoint) if the host closes it.

Game stats and player stats

During gameplay: Player entities (player:{id}) contain game-specific state including history, playerInfo, and state-specific data.

After game completion: The room entity may contain result data. Player entities may contain history arrays with per-round results. The analytics array in the room entity tracks game screens/phases.

Specific stat fields are game-dependent (Drawful 2, Quiplash, etc. have different schemas).


Passive Room Monitoring

It is possible to monitor a game room in real-time via WebSocket without joining as a player. There is no truly invisible WebSocket option (all connection types increment /connections), but there are approaches that don't consume a player slot or appear in the game UI.

Connect via the audience endpoint: wss://{host}/api/v2/audience/{code}/play

Pros:

  • Invisible to players — does not appear in the here map
  • Receives real-time entity broadcasts (room, textDescriptions, audiencePlayer)
  • Does not count toward maxPlayers or trigger full: true
  • No game UI impact (players don't see an audience member join unless the game has audience UI)

Cons:

  • Increments the raw /connections counter by 1 (slot held after disconnect)
  • here is null — cannot directly see the player list or their roles
  • Cannot query entities (opcodes like object/get and room/get-audience return "invalid opcode")
  • Limited to passively receiving whatever entities the host broadcasts

What you receive:

  • client/welcome with full entity snapshot (room, audience, textDescriptions, audiencePlayer)
  • Real-time object updates when entities change (player joins via textDescriptions, lobby state changes via room, game start/end)

Option 2: Shard Connection

Connect via: wss://{host}/api/v2/rooms/{code}/play?role=shard&name={name}&userId={userId}&format=json

Pros:

  • Gets the full here map with all connections and their roles (can filter for player roles to count players)
  • Can actively query entities with object/get and room/get-audience
  • Receives real-time entity broadcasts and periodic shard/sync CRDT data
  • Does not count toward maxPlayers or trigger full: true

Cons:

  • Visible to other clients — appears in the here map as {roles: {shard: {}}}
  • Increments the raw /connections counter (slot held after disconnect)
  • Other players' here field includes shard connections, which could confuse client code that counts here entries

Option 3: REST-Only Polling (truly invisible)

Poll REST endpoints (/rooms/{code}, /rooms/{code}/connections) periodically.

Pros:

  • Zero footprint — no WebSocket connection, no slot consumed, no /connections impact

Cons:

  • No real-time events — must poll on an interval
  • Cannot detect fine-grained state transitions between polls

Recommendation

For the game picker use case, the audience connection is the best balance:

  1. It's invisible to players (not in here, no game UI effect)
  2. It provides real-time entity updates (lobby state, player joins, game start/end)
  3. The only side effect is +1 on the raw /connections counter, which can be accounted for
  4. Use full from the REST API for slot availability — this flag only counts player-role connections and is unaffected by audience/shard connections

For player counting with an audience monitor connected: either use full / maxPlayers from REST (unaffected), or use connections - 2 (subtract host + your monitor) from the /connections endpoint.


Appendix: Raw Captures

Player client/welcome (initial connection)

{
  "pc": 564,
  "opcode": "client/welcome",
  "result": {
    "id": 2,
    "name": "PROBE1",
    "secret": "14496f20-83ed-4d1f-82bd-f2131f3c3c50",
    "reconnect": false,
    "deviceId": "0209922833.5aebbb0024f7bc2a3342d1",
    "entities": {
      "audience": ["audience/pn-counter", {"key": "audience", "count": 0}, {"locked": false}],
      "room": ["object", {"key": "room", "val": {"state": "Lobby", "lobbyState": "WaitingForMore", "..."}, "version": 0, "from": 1}, {"locked": false}],
      "textDescriptions": ["object", {"key": "textDescriptions", "val": {"..."}, "version": 0, "from": 1}, {"locked": false}]
    },
    "here": {
      "1": {"id": 1, "roles": {"host": {}}}
    },
    "profile": {
      "id": 2,
      "roles": {"player": {"name": "PROBE1"}}
    }
  }
}

Audience client/welcome

{
  "pc": 777,
  "opcode": "client/welcome",
  "result": {
    "id": 4000001355,
    "secret": "10affd0afb35892e73051b15",
    "reconnect": false,
    "entities": {
      "audience": ["audience/pn-counter", {"key": "audience", "count": 0}, {"locked": false}],
      "audiencePlayer": ["object", {"key": "audiencePlayer", "val": {"..."}, "version": 1, "from": 1}, {"locked": false}],
      "room": ["object", {"key": "room", "val": {"..."}, "version": 4, "from": 1}, {"locked": false}],
      "textDescriptions": ["object", {"key": "textDescriptions", "val": {"..."}, "version": 2, "from": 1}, {"locked": false}]
    },
    "here": null,
    "profile": null
  }
}

Key differences from player welcome:

  • id is in the 4 billion range (separated from player ID space)
  • here is null (audience can't see the player list)
  • profile is null (audience has no player profile)
  • Includes audiencePlayer entity (audience-specific state)

Shard client/welcome

{
  "opcode": "client/welcome",
  "result": {
    "id": 7,
    "secret": "82608030-59fb-46e3-b4ab-b23c7f733526",
    "reconnect": false,
    "entities": {
      "audience": ["crdt/pn-counter", [2,1,1,3,1,1,4,1,1], {"locked": false}],
      "audiencePlayer": ["object", {"key": "audiencePlayer", "val": {"..."}, "version": 1, "from": 1}, {"locked": false}],
      "room": ["object", {"key": "room", "val": {"..."}, "version": 0, "from": 1}, {"locked": false}],
      "scores": ["crdt/rankset", {"ranks": []}, {"locked": false}],
      "textDescriptions": ["object", {"key": "textDescriptions", "val": {"..."}, "version": 0, "from": 1}, {"locked": false}]
    },
    "here": {
      "1": {"id": 1, "roles": {"host": {}}},
      "2": {"id": 2, "roles": {"shard": {}}},
      "8": {"id": 8, "roles": {"player": {"name": "TestPlayer1"}}}
    },
    "profile": null
  }
}

Key differences from player welcome:

  • id is a low single-digit number (separate from player/audience ID spaces)
  • Gets the raw CRDT representation of audience counter instead of the friendly {count: N} form
  • Gets additional entities like scores (CRDT rankset)
  • here includes ALL connections with their roles (host, players, other shards)
  • profile is null (shard has no player profile)
  • Receives periodic shard/sync messages with CRDT state updates

Reconnection client/welcome

{
  "pc": 641,
  "opcode": "client/welcome",
  "result": {
    "id": 2,
    "name": "PROBE1",
    "secret": "14496f20-83ed-4d1f-82bd-f2131f3c3c50",
    "reconnect": true,
    "deviceId": "0209922833.5aebbb0024f7bc2a3342d1",
    "entities": { "..." },
    "here": { "..." },
    "profile": { "..." }
  }
}

The reconnect: true flag distinguishes reconnections from new joins. Same id and secret are preserved.

Entity Update (player join observed by existing player)

{
  "pc": 729,
  "opcode": "object",
  "result": {
    "key": "textDescriptions",
    "val": {
      "data": {"TEXT_DESCRIPTION_PLAYER_VIP": "PROBE1"},
      "latestDescriptions": [
        {"category": "TEXT_DESCRIPTION_PLAYER_JOINED", "id": 2, "text": "PROBE3 joined."}
      ]
    },
    "version": 2,
    "from": 1
  }
}

Room State Transition (lobby → can start)

{
  "pc": 735,
  "opcode": "object",
  "result": {
    "key": "room",
    "val": {
      "gameCanStart": true,
      "gameFinished": false,
      "gameIsStarting": false,
      "lobbyState": "CanStart",
      "state": "Lobby"
    },
    "version": 5,
    "from": 1
  }
}

Error Response

{
  "pc": 1600,
  "opcode": "error",
  "result": {
    "code": 2003,
    "msg": "invalid opcode"
  }
}

Client → Server Request (object/get)

{
  "seq": 1,
  "opcode": "object/get",
  "params": {
    "key": "room"
  }
}

Registered User Roles in here

{
  "1": {"id": 1, "roles": {"host": {}}},
  "2": {"id": 2, "roles": {"player": {"name": "P1"}}},
  "3": {"id": 3, "roles": {"player": {"name": "P2"}}},
  "4": {"id": 4, "roles": {"player": {"name": "P3"}}}
}

Known roles:

  • host — The game console/PC running the game
  • player — A player joined via jackbox.tv (includes name)
  • shard — Internal/infrastructure connection. Externally connectable with role=shard. Gets full entity access (read-only), appears in here map, receives shard/sync CRDT data. IDs are in the low single-digit range.
  • observer — Internal role (password-protected, not publicly accessible)
  • moderator — Moderation role (requires moderationEnabled: true on the room)

here includes disconnected players. Tested by connecting 3 players, closing all their WebSockets, then having a new player connect. The new player's here field listed all 3 previous players despite none of them having active WebSocket connections. This is consistent with Jackbox's slot-reservation model where player slots persist for reconnection.