# 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](#overview) 2. [REST API Reference](#rest-api-reference) 3. [WebSocket Protocol Reference](#websocket-protocol-reference) 4. [Entity Model](#entity-model) 5. [Game Lifecycle](#game-lifecycle) 6. [Player & Room Management](#player--room-management) 7. [Appendix: Raw Captures](#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). --- ## 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:** ```json { "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:** ```json { "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 allocated slot count. Includes all connection types (host, players, audience, shards). **Response:** `{"ok": true, "body": {"connections": 3}}` > **Important:** This counts allocated **slots**, not currently-active WebSocket connections. Jackbox holds player slots open indefinitely for reconnection — there is no concept of "leaving" a game. A player who closes their browser tab still occupies a slot. The count only decreases if the room itself is destroyed. Therefore, `connections - 1` gives the number of players who have **ever joined** the room, not the number currently online. ### POST /api/v2/rooms Create a new room. Returns a room code and management token. **Request:** ```json { "appTag": "drawful2international", "userId": "your-user-id" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `appTag` | string | Yes | Game identifier | | `userId` | string | Yes | Creator's user ID | **Response:** ```json { "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. **Required WebSocket sub-protocol:** `ecast-v0` **Required headers:** | Header | Value | |--------|-------| | `Origin` | `https://jackbox.tv` | | `Sec-WebSocket-Protocol` | `ecast-v0` | **Example (Node.js with `ws`):** ```javascript const ws = new WebSocket(url, ['ecast-v0'], { headers: { 'Origin': 'https://jackbox.tv' } }); ``` ### Message Format **Client → Server:** ```json { "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:** ```json { "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": , "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 | | 2007 | `entity value is not of type {expected}` | Type mismatch (e.g., using `text/get` on an `object` entity) | | 2023 | `permission denied` | Operation not allowed for this client's role | | 2027 | `the room has already been closed` | Room ended but REST API may still return room info (REST/WS state divergence) | | — | `only the host can close the room` | Specific error for `room/exit` from non-host | --- ## 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: ```json ["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: ```json { "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. ```json { "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`. ```json { "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": "
please draw:
a picture of yourself
"}, "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. ```json { "key": "audience", "count": 0 } ``` #### `textDescriptions` (type: object) Human-readable event descriptions (join/leave notifications). ```json { "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. ```json { "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 (or lack thereof) **Jackbox has no concept of "leaving" or "disconnecting."** When a player's WebSocket closes (gracefully or otherwise), their player slot is held open indefinitely for reconnection. From the game's perspective, the player is still "in the game" — just temporarily unreachable. 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 disconnected players. - 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. The only way a player slot is freed is if the room itself is destroyed (host closes the game) or the room times out. > **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 or leave **Join detection:** - **WebSocket (best):** Watch for `textDescriptions` entity updates with `category: "TEXT_DESCRIPTION_PLAYER_JOINED"` or `"TEXT_DESCRIPTION_PLAYER_JOINED_VIP"`. - **WebSocket (initial):** The `here` field in `client/welcome` lists all registered players (see caveats below). - **REST (polling):** `GET /api/v2/rooms/{code}/connections` — count increases when a new player joins. **Leave detection — the hard problem:** Jackbox does not have a concept of "leaving." Player slots are held open indefinitely for reconnection. There is: - **No** `client/disconnected` event sent to players. - **No** change in the `connections` REST count when a player's WebSocket closes. - **No** `textDescriptions` update for disconnections. - **No** change in the `here` field — disconnected players remain listed. In other words, **the ecast API provides no mechanism to detect that a player has disconnected.** The game treats all players as permanently in the room once joined. This is by design — the Jackbox UI has no "leave" button; a player who closes their browser can always reconnect. **Possible workarounds:** - Monitor game-specific player entity state changes (e.g., a drawing game might detect timeouts for players who don't submit). - The game host (console) may have internal logic to handle absent players, but this is not exposed through the ecast API. ### How to count players **Counting total player slots (who has ever joined):** **Method 1 — REST `/connections`:** ``` GET /api/v2/rooms/{code}/connections → {"connections": N} ``` `N` = host(1) + all player slots ever allocated + audience + shards. This count **does not decrease** when players disconnect. `N - 1` gives the approximate number of player slots allocated. **Method 2 — WebSocket `here` field:** The `client/welcome` message includes a `here` object mapping session IDs to roles: ```json { "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`. This includes **both connected and disconnected** players — `here` reflects all allocated slots, not just active WebSocket connections. The `here` field excludes the connecting client itself (you don't see yourself). **Counting currently-connected players:** There is no reliable API method to distinguish between connected and disconnected players. The `connections` count and `here` field both include disconnected players whose slots are held for reconnection. **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). --- ## Appendix: Raw Captures ### Player `client/welcome` (initial connection) ```json { "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` ```json { "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) ### Reconnection `client/welcome` ```json { "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) ```json { "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) ```json { "pc": 735, "opcode": "object", "result": { "key": "room", "val": { "gameCanStart": true, "gameFinished": false, "gameIsStarting": false, "lobbyState": "CanStart", "state": "Lobby" }, "version": 5, "from": 1 } } ``` ### Error Response ```json { "pc": 1600, "opcode": "error", "result": { "code": 2003, "msg": "invalid opcode" } } ``` ### Client → Server Request (object/get) ```json { "seq": 1, "opcode": "object/get", "params": { "key": "room" } } ``` ### Registered User Roles in `here` ```json { "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 connection (appears when audience is connected, possibly audience aggregator) > **`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.