Files
jackboxpartypack-gamepicker/docs/api/websocket.md
cottongin 8ba32e128c docs: comprehensive API documentation from source code
Replace existing docs with fresh documentation built entirely from source
code analysis. OpenAPI 3.1 spec as source of truth, plus human-readable
Markdown with curl examples, response samples, and workflow guides.

- OpenAPI 3.1 spec covering all 42 endpoints (validated against source)
- 7 endpoint reference docs (auth, games, sessions, picker, stats, votes, webhooks)
- WebSocket protocol documentation (auth, subscriptions, 4 event types)
- 4 guide documents (getting started, session lifecycle, voting, webhooks)
- API README with overview, auth docs, and quick reference table
- Old docs archived to docs/archive/

Made-with: Cursor
2026-03-15 16:44:53 -04:00

9.5 KiB

WebSocket Protocol

1. Overview

The WebSocket API provides real-time updates for Jackbox gaming sessions. Use it to:

  • Receive notifications when sessions start, end, or when games are added
  • Track player counts as they are updated
  • Avoid polling REST endpoints for session state changes

The WebSocket server runs on the same host and port as the HTTP API. Connect to /api/sessions/live to establish a live connection.


2. Connection Setup

URL: ws://host:port/api/sessions/live

  • Use ws:// for HTTP and wss:// for HTTPS
  • No query parameters are required
  • Connection can be established without authentication (auth happens via a message after connect)

JavaScript example:

const host = 'localhost';
const port = 5000;
const protocol = 'ws';
const ws = new WebSocket(`${protocol}://${host}:${port}/api/sessions/live`);

ws.onopen = () => {
  console.log('Connected');
};

3. Authentication

Authentication is required for subscribing to sessions and for receiving most events. Send your JWT token in an auth message after connecting.

Send (client → server):

{ "type": "auth", "token": "<jwt>" }

Success response:

{ "type": "auth_success", "message": "Authenticated successfully" }

Failure responses:

{ "type": "auth_error", "message": "Invalid or expired token" }
{ "type": "auth_error", "message": "Token required" }

JavaScript example:

// After opening the connection...
ws.send(JSON.stringify({
  type: 'auth',
  token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
}));

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  if (msg.type === 'auth_success') {
    console.log('Authenticated');
  } else if (msg.type === 'auth_error') {
    console.error('Auth failed:', msg.message);
  }
};

Obtain a JWT by calling POST /api/auth/login with your admin key.


4. Message Types — Client to Server

Type Required Fields Description
auth token Authenticate with a JWT
subscribe sessionId Subscribe to a session's events
unsubscribe sessionId Unsubscribe from a session
ping Heartbeat; server responds with pong

auth

{ "type": "auth", "token": "<jwt>" }

subscribe

Must be authenticated. You can subscribe to multiple sessions.

{ "type": "subscribe", "sessionId": 3 }

unsubscribe

Must be authenticated.

{ "type": "unsubscribe", "sessionId": 3 }

ping

{ "type": "ping" }

5. Message Types — Server to Client

Type Description
auth_success Authentication succeeded
auth_error Authentication failed
subscribed Successfully subscribed to a session
unsubscribed Successfully unsubscribed from a session
pong Response to client ping
error General error (e.g., not authenticated)
session.started New session created (broadcast to all authenticated clients)
game.added Game added to a session (broadcast to subscribers)
session.ended Session closed (broadcast to subscribers)
player-count.updated Player count changed (broadcast to subscribers)

6. Event Reference

All server-sent events use this envelope:

{
  "type": "<event-type>",
  "timestamp": "2026-03-15T20:30:00.000Z",
  "data": { ... }
}

session.started

  • Broadcast to: All authenticated clients (not session-specific)
  • Triggered by: POST /api/sessions (creating a new session)

Data:

{
  "session": {
    "id": 3,
    "is_active": 1,
    "created_at": "2026-03-15T20:00:00",
    "notes": "Friday game night"
  }
}

game.added

  • Broadcast to: Clients subscribed to the session
  • Triggered by: POST /api/sessions/{id}/games (adding a game)

Data:

{
  "session": {
    "id": 3,
    "is_active": true,
    "games_played": 5
  },
  "game": {
    "id": 42,
    "title": "Quiplash 3",
    "pack_name": "Jackbox Party Pack 7",
    "min_players": 3,
    "max_players": 8,
    "manually_added": false,
    "room_code": "ABCD"
  }
}

session.ended

  • Broadcast to: Clients subscribed to the session
  • Triggered by: POST /api/sessions/{id}/close (closing a session)

Data:

{
  "session": {
    "id": 3,
    "is_active": 0,
    "games_played": 8
  }
}

player-count.updated

  • Broadcast to: Clients subscribed to the session
  • Triggered by: PATCH /api/sessions/{sessionId}/games/{sessionGameId}/player-count

Data:

{
  "sessionId": "3",
  "gameId": "7",
  "playerCount": 6,
  "status": "completed"
}

7. Error Handling

Type Message When
error Not authenticated subscribe/unsubscribe without auth
error Session ID required subscribe without sessionId
error Unknown message type: foo Unknown type in client message
error Invalid message format Unparseable or non-JSON message
auth_error Token required auth without token
auth_error Invalid or expired token auth with invalid/expired JWT

8. Heartbeat and Timeout

  • Client → Server: Send { "type": "ping" } periodically
  • Server → Client: Responds with { "type": "pong" }
  • Timeout: If no ping is received for 60 seconds, the server terminates the connection
  • Server check: The server checks for stale connections every 30 seconds

Implement a heartbeat on the client to keep the connection alive:

let pingInterval;

function startHeartbeat() {
  pingInterval = setInterval(() => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify({ type: 'ping' }));
    }
  }, 30000); // every 30 seconds
}

ws.onopen = () => {
  startHeartbeat();
};

ws.onclose = () => {
  clearInterval(pingInterval);
};

9. Reconnection

The server does not maintain state across disconnects. After reconnecting:

  1. Re-authenticate with an auth message
  2. Re-subscribe to any sessions you were tracking

Implement exponential backoff for reconnection attempts:

let reconnectAttempts = 0;
const maxReconnectAttempts = 10;
const baseDelay = 1000;

function connect() {
  const ws = new WebSocket('ws://localhost:5000/api/sessions/live');

  ws.onopen = () => {
    reconnectAttempts = 0;
    ws.send(JSON.stringify({ type: 'auth', token: jwt }));
    // After auth_success, re-subscribe to sessions...
  };

  ws.onclose = () => {
    if (reconnectAttempts < maxReconnectAttempts) {
      const delay = Math.min(baseDelay * Math.pow(2, reconnectAttempts), 60000);
      reconnectAttempts++;
      setTimeout(connect, delay);
    }
  };
}

connect();

10. Complete Example

Full session lifecycle from connect to disconnect:

const JWT = 'your-jwt-token';
const WS_URL = 'ws://localhost:5000/api/sessions/live';

const ws = new WebSocket(WS_URL);
let pingInterval;
let subscribedSessions = new Set();

function send(msg) {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify(msg));
  }
}

ws.onopen = () => {
  console.log('Connected');
  send({ type: 'auth', token: JWT });

  pingInterval = setInterval(() => {
    if (ws.readyState === WebSocket.OPEN) {
      send({ type: 'ping' });
    }
  }, 30000);
};

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);

  switch (msg.type) {
    case 'auth_success':
      console.log('Authenticated');
      send({ type: 'subscribe', sessionId: 3 });
      break;

    case 'auth_error':
      console.error('Auth failed:', msg.message);
      break;

    case 'subscribed':
      subscribedSessions.add(msg.sessionId);
      console.log('Subscribed to session', msg.sessionId);
      break;

    case 'unsubscribed':
      subscribedSessions.delete(msg.sessionId);
      console.log('Unsubscribed from session', msg.sessionId);
      break;

    case 'pong':
      // Heartbeat acknowledged
      break;

    case 'session.started':
      console.log('New session:', msg.data.session);
      break;

    case 'game.added':
      console.log('Game added:', msg.data.game.title, 'to session', msg.data.session.id);
      break;

    case 'session.ended':
      console.log('Session ended:', msg.data.session.id);
      subscribedSessions.delete(msg.data.session.id);
      break;

    case 'player-count.updated':
      console.log('Player count:', msg.data.playerCount, 'for game', msg.data.gameId);
      break;

    case 'error':
    case 'auth_error':
      console.error('Error:', msg.message);
      break;

    default:
      console.log('Unknown message:', msg);
  }
};

ws.onerror = (err) => console.error('WebSocket error:', err);
ws.onclose = () => {
  clearInterval(pingInterval);
  console.log('Disconnected');
};

// Later: unsubscribe and close
function disconnect() {
  subscribedSessions.forEach((sessionId) => {
    send({ type: 'unsubscribe', sessionId });
  });
  ws.close();
}