Adds complete documentation of the ecast platform covering: - REST API (8 endpoints including newly discovered /connections, /info, /status) - WebSocket protocol (connection, message format, 40+ opcodes) - Entity model (room, player, audience, textDescriptions) - Game lifecycle (lobby → start → gameplay → end) - Player/room management answers (counting, join/leave detection, etc.) Also adds scripts/ws-probe.js utility for direct WebSocket probing. Made-with: Cursor
28 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
- Overview
- REST API Reference
- WebSocket Protocol Reference
- Entity Model
- Game Lifecycle
- Player & Room Management
- 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 thehost/audienceHostfields 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:
{
"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:
numAudiencemay not update in real-time for WebSocket-connected audience members. Consider using the WebSocketroom/get-audienceopcode for live counts.
GET /api/v2/rooms/{code}/connections
Active connection count. Includes all connection types (host, players, audience, shards).
Response: {"ok": true, "body": {"connections": 3}}
Important: This is the most reliable REST-based way to get a real-time count of connected entities. Subtract 1 for the host connection. Note that ungracefully disconnected clients may linger for up to 30-60 seconds before the server detects the dead connection.
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:
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) |
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):
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. |
client/disconnected |
S→C | Notification that another client disconnected. |
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. |
Note on
client/connectedandclient/disconnected: These opcodes exist in the client JavaScript but were not observed during testing with player-role connections. They may only be delivered to the host connection, or may require specific server configuration.
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 |
| 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 |
| — | 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:
["object", {"key": "room", "val": {...}, "version": 0, "from": 1}, {"locked": false}]
- Type identifier (string):
"object","audience/pn-counter", etc. - Entity data (object):
key,val,version,from(originating client ID) - Metadata (object):
lockedflag
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:
objectupdate ontextDescriptionswith"X joined."inlatestDescriptionsobjectupdate onroom(e.g.,lobbyStatechanges 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:
lobbyState: "WaitingForMore"→ Fewer than minimum playerslobbyState: "CanStart"/gameCanStart: true→ Minimum players met, VIP can startgameIsStarting: true/lobbyState: "Countdown"→ Start countdown activestate: "Gameplay"→ Game has started- REST:
locked: true→ Room is locked, no new players can join
Game End
room.gameFinished: true→ Game is completeroom.statereturns to"Lobby"(for "same players" / "new players" options)- Player entities may contain
historyand result data
Reconnection
If a player reconnects with the same deviceId (browser cookie/localStorage), the server sends client/welcome with:
reconnect: true- Same
idandsecretas the original session - Full current state of all entities (not just deltas)
Disconnection
Graceful disconnection (WebSocket close code 1000) is processed immediately. Ungraceful disconnection (process kill, network drop) takes 30-60 seconds for the server to detect.
The connections REST endpoint reflects connection count changes. The here field in client/welcome shows stale connections until the server cleans them up.
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
textDescriptionsentity updates withcategory: "TEXT_DESCRIPTION_PLAYER_JOINED"or"TEXT_DESCRIPTION_PLAYER_JOINED_VIP". - WebSocket (initial): The
herefield inclient/welcomelists all currently connected users on first connect. - REST (polling):
GET /api/v2/rooms/{code}/connectionsreturns the total connection count.
Leave detection:
- REST (polling): Poll
GET /api/v2/rooms/{code}/connectionsand compare counts. This is currently the most reliable method. - WebSocket:
client/disconnectedopcode exists in the protocol but was not reliably observed during testing. May only be sent to the host. - Caveat: Ungraceful disconnects can take 30-60 seconds to be reflected.
How to count players
Method 1 — REST /connections (simplest, recommended):
GET /api/v2/rooms/{code}/connections → {"connections": N}
N includes host (1) + players + audience + internal connections (shards). To get player count: N - 1 - audienceCount - shardCount. For a rough estimate without audience: N - 1.
Method 2 — WebSocket here field (most accurate at connection time):
The client/welcome message includes a here object mapping session IDs to roles:
{
"1": {"id": 1, "roles": {"host": {}}},
"2": {"id": 2, "roles": {"player": {"name": "PROBE1"}}},
"3": {"id": 3, "roles": {"player": {"name": "PROBE3"}}},
"4": {"id": 4, "roles": {"shard": {}}}
}
Count entries where roles contains player. This gives exact player count at connection time but doesn't update in real-time.
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:
lobbyStatetransitions:"WaitingForMore"→"CanStart"→"Countdown"→ (game starts)gameCanStart: true→ minimum player count metgameIsStarting: true→ start countdown activestatechanges 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)
{
"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:
idis in the 4 billion range (separated from player ID space)hereisnull(audience can't see the player list)profileisnull(audience has no player profile)- Includes
audiencePlayerentity (audience-specific state)
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"
}
}
Connected User Roles in here
{
"1": {"id": 1, "roles": {"host": {}}},
"2": {"id": 2, "roles": {"player": {"name": "PROBE1"}}},
"3": {"id": 3, "roles": {"player": {"name": "PROBE3"}}},
"4": {"id": 4, "roles": {"shard": {}}}
}
Known roles:
host— The game console/PC running the gameplayer— A player joined via jackbox.tv (includesname)shard— Internal connection (possibly audience aggregator)