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.
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).
- **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/` |
- **`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.
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.
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.
| `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.
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.
> **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.
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):**
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:
**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.
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.
- **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.
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.
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`).
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.
**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).
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.
### Option 1: Audience Connection (recommended)
Connect via the audience endpoint: `wss://{host}/api/v2/audience/{code}/play`
**Pros:**
- Invisible to players — does not appear in the `here` map
- 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.
-`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.