WebSocket API Reference

Updated: May 11, 2025 API Version: v1

Introduction

The MyAppAPI WebSocket API provides real-time communication capabilities for your applications. Unlike the request-response pattern of REST APIs, WebSockets enable persistent, bidirectional communication channels that allow both the client and server to send messages at any time.

This makes WebSockets ideal for applications that require real-time updates, notifications, live data feeds, collaborative features, or any scenario where low-latency, bidirectional communication is essential.

When to Use WebSockets

WebSockets are particularly valuable for:

  • Real-time dashboards and analytics
  • Live feeds and notifications
  • Chat and messaging applications
  • Collaborative editing tools
  • Real-time multiplayer games
  • Financial trading platforms
  • IoT device monitoring

WebSockets vs. REST and GraphQL

MyAppAPI offers multiple API access methods (REST, GraphQL, and WebSockets) to fit different application requirements. Here's how they compare:

Feature REST API GraphQL API WebSocket API
Communication Pattern Request-response Request-response Bidirectional, persistent
Connection Type New connection per request New connection per request Persistent connection
Real-time Updates Polling required Subscriptions via separate WebSocket Native real-time
Data Efficiency Medium (HTTP overhead per request) High (specific data selection) Highest (minimal framing overhead)
Latency Higher (connection setup each time) Higher (connection setup each time) Lowest (persistent connection)
Best For CRUD operations, standard integrations Complex data requirements, flexible queries Real-time data, notifications, live updates

Choosing the Right API

Many applications benefit from combining these APIs for different purposes:

  • Use REST for standard CRUD operations and admin functions
  • Use GraphQL for complex data fetching and flexible queries
  • Use WebSockets for real-time updates, notifications, and live data feeds

Establishing Connections

To establish a WebSocket connection with the MyAppAPI WebSocket server, connect to the following endpoint:

wss://ws.myappapi.com/v1/

For regional endpoints, use:

Region WebSocket Endpoint
United States (Default) wss://ws.myappapi.com/v1/
European Union wss://eu.ws.myappapi.com/v1/
Asia-Pacific wss://ap.ws.myappapi.com/v1/

Important

Always use secure WebSocket connections (wss://) in production environments to ensure data encryption during transmission.

Connection Parameters

You can provide optional query parameters when establishing the connection:

Parameter Description Required
v API version (defaults to the latest version) No
client_id A unique identifier for the client (useful for connection tracking) No
wss://ws.myappapi.com/v1/?client_id=your_client_identifier

Authentication

WebSocket connections require authentication to identify the requesting user or application. MyAppAPI supports multiple authentication methods for WebSockets.

Query Parameter Authentication

The simplest method is to include your API key as a query parameter when establishing the connection:

wss://ws.myappapi.com/v1/?api_key=your_api_key_here

Security Consideration

While convenient, query parameter authentication may expose your API key in server logs. For production environments, consider using one of the more secure methods below.

Authentication Message

A more secure approach is to establish the connection first, then send an authentication message immediately after connection:

{
  "type": "auth",
  "api_key": "your_api_key_here"
}
const socket = new WebSocket('wss://ws.myappapi.com/v1/');

socket.onopen = () => {
  // Send authentication message immediately after connection
  socket.send(JSON.stringify({
    type: 'auth',
    api_key: 'your_api_key_here'
  }));
};

socket.onmessage = (event) => {
  const data = JSON.parse(event.data);
  
  if (data.type === 'auth_success') {
    console.log('Authentication successful!');
    // Now you can subscribe to events
  } else if (data.type === 'auth_error') {
    console.error('Authentication failed:', data.message);
  }
};

JWT Authentication

For applications using JWT authentication, you can provide the JWT token:

{
  "type": "auth",
  "token": "your_jwt_token_here"
}
const socket = new WebSocket('wss://ws.myappapi.com/v1/');

socket.onopen = () => {
  // Send JWT authentication message
  socket.send(JSON.stringify({
    type: 'auth',
    token: 'your_jwt_token_here'
  }));
};

Authentication Timeout

You must authenticate within 30 seconds of establishing the connection, or the server will automatically close the connection.

Message Format

All messages exchanged over the WebSocket connection use JSON format. Each message has a specific structure depending on its type and purpose.

Common Message Structure

All messages, both sent and received, include a type field that indicates the purpose of the message:

{
  "type": "message_type",
  // Additional fields specific to the message type
}

Message Types

The WebSocket API uses the following common message types:

Type Direction Description
auth Client → Server Authenticate the WebSocket connection
auth_success Server → Client Authentication was successful
auth_error Server → Client Authentication failed
subscribe Client → Server Subscribe to an event or data channel
unsubscribe Client → Server Unsubscribe from an event or data channel
subscription_success Server → Client Subscription was successful
subscription_error Server → Client Subscription failed
event Server → Client An event has occurred (data update, notification, etc.)
ping Client → Server Check connection status
pong Server → Client Response to a ping message
error Server → Client General error message

Request IDs

To correlate requests with responses, you can include a request_id field in your messages. The server will include the same request_id in its response:

{
  "type": "subscribe",
  "channel": "projects.updates",
  "project_id": "proj_12345",
  "request_id": "req_abc123"
}
{
  "type": "subscription_success",
  "channel": "projects.updates",
  "project_id": "proj_12345",
  "subscription_id": "sub_xyz789",
  "request_id": "req_abc123"
}

Event Subscriptions

The primary purpose of the WebSocket API is to subscribe to real-time events and updates. You can subscribe to various event channels based on your application's needs.

Subscribing to Events

To subscribe to events, send a subscribe message specifying the channel and any required parameters:

{
  "type": "subscribe",
  "channel": "projects.updates",
  "project_id": "proj_12345",
  "request_id": "req_abc123"
}

If the subscription is successful, you'll receive a subscription_success response:

{
  "type": "subscription_success",
  "channel": "projects.updates",
  "project_id": "proj_12345",
  "subscription_id": "sub_xyz789",
  "request_id": "req_abc123"
}

Available Event Channels

MyAppAPI offers various event channels you can subscribe to:

Channel Description Required Parameters
users.presence User online/offline status changes None (for all users you can access) or user_id
projects.updates Project creation, updates, or deletion None (for all projects) or project_id
tasks.updates Task creation, updates, status changes, or deletion project_id (optional: task_id)
comments.updates New comments or comment edits project_id or task_id
files.updates File uploads, updates, or deletion project_id (optional: file_id)
notifications User notifications (mentions, assignments, etc.) None (applies to authenticated user)
data.{resource_type} Real-time data feeds for specific resource types Varies by resource type

Receiving Events

When an event occurs that matches your subscription, you'll receive an event message:

{
  "type": "event",
  "channel": "projects.updates",
  "event": "project.updated",
  "subscription_id": "sub_xyz789",
  "timestamp": "2025-05-11T14:32:10.456Z",
  "data": {
    "project_id": "proj_12345",
    "name": "Updated Project Name",
    "description": "This project has been updated",
    "updated_by": "user_98765",
    "updated_at": "2025-05-11T14:32:10.456Z"
  }
}

Unsubscribing from Events

To stop receiving events for a specific subscription, send an unsubscribe message:

{
  "type": "unsubscribe",
  "subscription_id": "sub_xyz789",
  "request_id": "req_def456"
}

You'll receive a confirmation:

{
  "type": "unsubscription_success",
  "subscription_id": "sub_xyz789",
  "request_id": "req_def456"
}

Error Handling

When an error occurs, the WebSocket server sends an error message with details about what went wrong.

Error Message Format

{
  "type": "error",
  "code": "invalid_subscription",
  "message": "Unable to subscribe to the specified channel",
  "request_id": "req_abc123",
  "details": {
    "reason": "Missing required parameter: project_id",
    "channel": "tasks.updates"
  }
}

Common Error Codes

Error Code Description
authentication_required The connection is not authenticated
invalid_credentials The provided authentication credentials are invalid
permission_denied The authenticated user doesn't have permission for the requested action
invalid_message_format The message format is invalid or missing required fields
invalid_subscription The subscription request is invalid
subscription_limit_exceeded The maximum number of active subscriptions has been reached
rate_limit_exceeded The client has exceeded the rate limit for WebSocket messages
resource_not_found The requested resource does not exist
server_error An unexpected error occurred on the server

Handling Errors

Here's an example of how to handle WebSocket errors in your client application:

const socket = new WebSocket('wss://ws.myappapi.com/v1/');

socket.onmessage = (event) => {
  const data = JSON.parse(event.data);
  
  if (data.type === 'error') {
    console.error(`Error (${data.code}): ${data.message}`);
    
    // Handle specific error types
    switch (data.code) {
      case 'authentication_required':
        // Attempt to re-authenticate
        authenticateSocket();
        break;
        
      case 'invalid_subscription':
        // Fix subscription parameters and try again
        console.error('Subscription error details:', data.details);
        break;
        
      case 'rate_limit_exceeded':
        // Implement exponential backoff
        setTimeout(() => {
          retrySendMessage(data.request_id);
        }, calculateBackoff());
        break;
        
      case 'server_error':
        // Report to monitoring system
        reportServerError(data);
        break;
    }
  }
};
import json
import websocket
import time

def on_message(ws, message):
    data = json.loads(message)
    
    if data.get('type') == 'error':
        print(f"Error ({data.get('code')}): {data.get('message')}")
        
        # Handle specific error types
        error_code = data.get('code')
        
        if error_code == 'authentication_required':
            # Attempt to re-authenticate
            authenticate_socket(ws)
        
        elif error_code == 'invalid_subscription':
            # Fix subscription parameters and try again
            print('Subscription error details:', data.get('details'))
        
        elif error_code == 'rate_limit_exceeded':
            # Implement exponential backoff
            retry_delay = calculate_backoff()
            print(f"Rate limit exceeded. Retrying in {retry_delay} seconds.")
            time.sleep(retry_delay)
            retry_send_message(ws, data.get('request_id'))
        
        elif error_code == 'server_error':
            # Report to monitoring system
            report_server_error(data)

# Connect to WebSocket
ws = websocket.WebSocketApp("wss://ws.myappapi.com/v1/",
                          on_message=on_message)
ws.run_forever()

Rate Limits

To ensure fair usage and system stability, MyAppAPI implements rate limits on WebSocket connections and messages.

Connection Limits

Plan Maximum Connections Maximum Subscriptions per Connection
Free 5 10
Professional 25 50
Business 100 200
Enterprise Custom Custom

Message Rate Limits

Plan Messages per Minute per Connection
Free 60
Professional 300
Business 1,000
Enterprise Custom

Rate Limit Notifications

When you approach or exceed rate limits, the server will send warning messages. If limits are consistently exceeded, connections may be temporarily throttled or disconnected.

Best Practices for Rate Limits

  • Batch subscription requests when possible
  • Implement exponential backoff for retries
  • Monitor your usage to avoid hitting limits
  • Optimize your subscription strategy (subscribe only to what you need)
  • Consider upgrading your plan if you consistently approach limits

Connection Management

Proper connection management is essential for maintaining reliable WebSocket communications.

Ping/Pong for Connection Health

To verify that a connection is still active, you can send ping messages periodically:

{
  "type": "ping",
  "request_id": "ping_1"
}

The server will respond with a pong message:

{
  "type": "pong",
  "timestamp": "2025-05-11T14:45:23.789Z",
  "request_id": "ping_1"
}

Server-Initiated Pings

The server also sends periodic ping messages to check client health. Clients should respond with pong messages to avoid connection termination.

Reconnection Strategy

WebSocket connections may disconnect due to network issues or server maintenance. Implement a robust reconnection strategy:

class WebSocketClient {
  constructor(url) {
    this.url = url;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = 10;
    this.baseReconnectDelay = 1000; // 1 second
    this.maxReconnectDelay = 30000; // 30 seconds
    this.subscriptions = new Map();
    this.connect();
  }
  
  connect() {
    this.socket = new WebSocket(this.url);
    
    this.socket.onopen = () => {
      console.log('WebSocket connected');
      this.reconnectAttempts = 0;
      this.authenticate();
      this.restoreSubscriptions();
    };
    
    this.socket.onclose = (event) => {
      console.log(`WebSocket disconnected: ${event.code}`);
      this.scheduleReconnect();
    };
    
    this.socket.onerror = (error) => {
      console.error('WebSocket error:', error);
    };
    
    // Set up message handling
    this.socket.onmessage = this.handleMessage.bind(this);
  }
  
  scheduleReconnect() {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.error('Maximum reconnection attempts reached');
      return;
    }
    
    this.reconnectAttempts++;
    
    // Exponential backoff with jitter
    const delay = Math.min(
      this.baseReconnectDelay * Math.pow(2, this.reconnectAttempts - 1),
      this.maxReconnectDelay
    );
    const jitter = delay * 0.1 * Math.random();
    const reconnectDelay = delay + jitter;
    
    console.log(`Reconnecting in ${reconnectDelay}ms (attempt ${this.reconnectAttempts})`);
    
    setTimeout(() => {
      this.connect();
    }, reconnectDelay);
  }
  
  authenticate() {
    this.sendMessage({
      type: 'auth',
      api_key: this.apiKey
    });
  }
  
  restoreSubscriptions() {
    // Re-subscribe to all previous subscriptions
    for (const [channel, params] of this.subscriptions.entries()) {
      this.subscribe(channel, params);
    }
  }
  
  // Other methods (subscribe, unsubscribe, sendMessage, etc.)
}
import json
import threading
import time
import random
import websocket

class WebSocketClient:
    def __init__(self, url, api_key):
        self.url = url
        self.api_key = api_key
        self.reconnect_attempts = 0
        self.max_reconnect_attempts = 10
        self.base_reconnect_delay = 1  # 1 second
        self.max_reconnect_delay = 30  # 30 seconds
        self.subscriptions = {}
        self.ws = None
        self.connect()
        
    def connect(self):
        self.ws = websocket.WebSocketApp(
            self.url,
            on_open=self.on_open,
            on_message=self.on_message,
            on_error=self.on_error,
            on_close=self.on_close
        )
        
        # Start WebSocket connection in a separate thread
        self.thread = threading.Thread(target=self.ws.run_forever)
        self.thread.daemon = True
        self.thread.start()
        
    def on_open(self, ws):
        print("WebSocket connected")
        self.reconnect_attempts = 0
        self.authenticate()
        self.restore_subscriptions()
        
    def on_message(self, ws, message):
        data = json.loads(message)
        # Handle different message types
        
    def on_error(self, ws, error):
        print(f"WebSocket error: {error}")
        
    def on_close(self, ws, close_status_code, close_reason):
        print(f"WebSocket closed: {close_status_code} - {close_reason}")
        self.schedule_reconnect()
        
    def schedule_reconnect(self):
        if self.reconnect_attempts >= self.max_reconnect_attempts:
            print("Maximum reconnection attempts reached")
            return
            
        self.reconnect_attempts += 1
        
        # Exponential backoff with jitter
        delay = min(
            self.base_reconnect_delay * (2 ** (self.reconnect_attempts - 1)),
            self.max_reconnect_delay
        )
        jitter = delay * 0.1 * random.random()
        reconnect_delay = delay + jitter
        
        print(f"Reconnecting in {reconnect_delay} seconds (attempt {self.reconnect_attempts})")
        
        time.sleep(reconnect_delay)
        self.connect()
        
    def authenticate(self):
        self.send_message({
            "type": "auth",
            "api_key": self.api_key
        })
        
    def restore_subscriptions(self):
        # Re-subscribe to all previous subscriptions
        for channel, params in self.subscriptions.items():
            self.subscribe(channel, params)
            
    def send_message(self, message):
        if self.ws and self.ws.sock and self.ws.sock.connected:
            self.ws.send(json.dumps(message))
        else:
            print("Cannot send message: WebSocket not connected")

Graceful Disconnect

When your application no longer needs the WebSocket connection, close it gracefully to free up server resources:

// Unsubscribe from all subscriptions first
function disconnect() {
  // Unsubscribe from all active subscriptions
  for (const subscriptionId of activeSubscriptions) {
    socket.send(JSON.stringify({
      type: 'unsubscribe',
      subscription_id: subscriptionId
    }));
  }
  
  // Wait a moment for unsubscribe messages to be processed
  setTimeout(() => {
    // Close the connection
    socket.close(1000, 'Client disconnecting');
  }, 500);
}
def disconnect(ws, active_subscriptions):
    # Unsubscribe from all active subscriptions
    for subscription_id in active_subscriptions:
        ws.send(json.dumps({
            "type": "unsubscribe",
            "subscription_id": subscription_id
        }))
    
    # Wait a moment for unsubscribe messages to be processed
    time.sleep(0.5)
    
    # Close the connection
    ws.close()

Implementation Examples

Here are complete examples of how to implement WebSocket connections for common scenarios.

Real-Time Project Updates

This example shows how to subscribe to real-time updates for a specific project:

const API_KEY = 'your_api_key_here';
const PROJECT_ID = 'proj_12345';

// Connect to WebSocket
const socket = new WebSocket('wss://ws.myappapi.com/v1/');

// Track subscription IDs
let projectSubscriptionId = null;

// Set up event handlers
socket.onopen = () => {
  console.log('WebSocket connection established');
  
  // Authenticate
  socket.send(JSON.stringify({
    type: 'auth',
    api_key: API_KEY
  }));
};

socket.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log('Received message:', data);
  
  switch (data.type) {
    case 'auth_success':
      console.log('Authentication successful');
      
      // Subscribe to project updates
      socket.send(JSON.stringify({
        type: 'subscribe',
        channel: 'projects.updates',
        project_id: PROJECT_ID,
        request_id: 'req_project_updates'
      }));
      break;
      
    case 'subscription_success':
      console.log(`Subscribed to ${data.channel}`);
      
      if (data.request_id === 'req_project_updates') {
        projectSubscriptionId = data.subscription_id;
      }
      break;
      
    case 'event':
      if (data.channel === 'projects.updates') {
        handleProjectUpdate(data);
      }
      break;
      
    case 'error':
      console.error(`Error: ${data.message}`);
      break;
  }
};

socket.onerror = (error) => {
  console.error('WebSocket error:', error);
};

socket.onclose = (event) => {
  console.log(`WebSocket connection closed: ${event.code} - ${event.reason}`);
};

// Handle project update events
function handleProjectUpdate(data) {
  const { event, data: eventData } = data;
  
  switch (event) {
    case 'project.updated':
      console.log(`Project updated: ${eventData.name}`);
      updateProjectUI(eventData);
      break;
      
    case 'project.member_added':
      console.log(`New member added: ${eventData.user.name}`);
      updateMembersList(eventData);
      break;
      
    case 'project.deleted':
      console.log(`Project deleted`);
      redirectToProjectsList();
      break;
  }
}

// Clean up function
function cleanup() {
  if (projectSubscriptionId) {
    socket.send(JSON.stringify({
      type: 'unsubscribe',
      subscription_id: projectSubscriptionId
    }));
  }
  
  socket.close();
}
import json
import websocket
import threading
import time

API_KEY = 'your_api_key_here'
PROJECT_ID = 'proj_12345'

# Create WebSocket client
class ProjectUpdatesClient:
    def __init__(self):
        self.subscription_id = None
        self.ws = None
        self.connect()
        
    def connect(self):
        self.ws = websocket.WebSocketApp(
            'wss://ws.myappapi.com/v1/',
            on_open=self.on_open,
            on_message=self.on_message,
            on_error=self.on_error,
            on_close=self.on_close
        )
        
        # Start WebSocket connection in a separate thread
        self.ws_thread = threading.Thread(target=self.ws.run_forever)
        self.ws_thread.daemon = True
        self.ws_thread.start()
        
    def on_open(self, ws):
        print('WebSocket connection established')
        
        # Authenticate
        self.ws.send(json.dumps({
            'type': 'auth',
            'api_key': API_KEY
        }))
        
    def on_message(self, ws, message):
        data = json.loads(message)
        print('Received message:', data)
        
        if data['type'] == 'auth_success':
            print('Authentication successful')
            
            # Subscribe to project updates
            self.ws.send(json.dumps({
                'type': 'subscribe',
                'channel': 'projects.updates',
                'project_id': PROJECT_ID,
                'request_id': 'req_project_updates'
            }))
            
        elif data['type'] == 'subscription_success':
            print(f"Subscribed to {data['channel']}")
            
            if data['request_id'] == 'req_project_updates':
                self.subscription_id = data['subscription_id']
                
        elif data['type'] == 'event':
            if data['channel'] == 'projects.updates':
                self.handle_project_update(data)
                
        elif data['type'] == 'error':
            print(f"Error: {data['message']}")
        
    def on_error(self, ws, error):
        print('WebSocket error:', error)
        
    def on_close(self, ws, close_status_code, close_reason):
        print(f"WebSocket connection closed: {close_status_code} - {close_reason}")
        
    def handle_project_update(self, data):
        event = data['event']
        event_data = data['data']
        
        if event == 'project.updated':
            print(f"Project updated: {event_data['name']}")
            self.update_project_ui(event_data)
            
        elif event == 'project.member_added':
            print(f"New member added: {event_data['user']['name']}")
            self.update_members_list(event_data)
            
        elif event == 'project.deleted':
            print("Project deleted")
            self.redirect_to_projects_list()
    
    def update_project_ui(self, data):
        # Update UI with new project data
        pass
        
    def update_members_list(self, data):
        # Update UI with new member
        pass
        
    def redirect_to_projects_list(self):
        # Handle project deletion
        pass
        
    def cleanup(self):
        if self.subscription_id:
            self.ws.send(json.dumps({
                'type': 'unsubscribe',
                'subscription_id': self.subscription_id
            }))
            
        self.ws.close()
        
# Create and start client
client = ProjectUpdatesClient()

# Keep main thread running
try:
    while True:
        time.sleep(1)
except KeyboardInterrupt:
    client.cleanup()

Real-Time Chat Application

This example demonstrates how to implement a real-time chat using WebSockets:

class ChatClient {
  constructor(apiKey, roomId) {
    this.apiKey = apiKey;
    this.roomId = roomId;
    this.socket = null;
    this.subscriptionId = null;
    this.messageCallbacks = [];
    this.userStatusCallbacks = [];
    this.connectionState = 'disconnected';
    
    this.connect();
  }
  
  connect() {
    this.connectionState = 'connecting';
    this.socket = new WebSocket('wss://ws.myappapi.com/v1/');
    
    this.socket.onopen = () => {
      console.log('Chat WebSocket connection established');
      this.connectionState = 'authenticating';
      
      // Authenticate
      this.socket.send(JSON.stringify({
        type: 'auth',
        api_key: this.apiKey
      }));
    };
    
    this.socket.onmessage = (event) => {
      const data = JSON.parse(event.data);
      
      switch (data.type) {
        case 'auth_success':
          this.connectionState = 'authenticated';
          this.subscribeToChat();
          break;
          
        case 'subscription_success':
          if (data.channel === 'chat.messages') {
            this.connectionState = 'connected';
            this.subscriptionId = data.subscription_id;
            this.notifyConnectionStatus(true);
          }
          break;
          
        case 'event':
          if (data.channel === 'chat.messages') {
            if (data.event === 'chat.message_received') {
              this.notifyNewMessage(data.data);
            } else if (data.event === 'chat.user_presence_changed') {
              this.notifyUserStatus(data.data);
            }
          }
          break;
          
        case 'error':
          console.error(`Chat WebSocket error: ${data.message}`);
          this.notifyError(data);
          break;
      }
    };
    
    this.socket.onerror = (error) => {
      console.error('Chat WebSocket error:', error);
      this.connectionState = 'error';
      this.notifyConnectionStatus(false, 'error');
    };
    
    this.socket.onclose = (event) => {
      console.log(`Chat WebSocket closed: ${event.code} - ${event.reason}`);
      this.connectionState = 'disconnected';
      this.notifyConnectionStatus(false, 'closed');
      
      // Attempt to reconnect after delay
      setTimeout(() => {
        if (this.connectionState === 'disconnected') {
          this.connect();
        }
      }, 5000);
    };
  }
  
  subscribeToChat() {
    this.socket.send(JSON.stringify({
      type: 'subscribe',
      channel: 'chat.messages',
      room_id: this.roomId,
      request_id: `chat_${this.roomId}`
    }));
  }
  
  sendMessage(message) {
    if (this.connectionState !== 'connected') {
      throw new Error('Not connected to chat');
    }
    
    // Send message via REST API
    return fetch('https://api.myappapi.com/v1/chats/messages', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-API-Key': this.apiKey
      },
      body: JSON.stringify({
        room_id: this.roomId,
        content: message
      })
    }).then(response => response.json());
  }
  
  onMessage(callback) {
    this.messageCallbacks.push(callback);
    return this;
  }
  
  onUserStatus(callback) {
    this.userStatusCallbacks.push(callback);
    return this;
  }
  
  onConnectionChange(callback) {
    this.connectionCallbacks.push(callback);
    return this;
  }
  
  notifyNewMessage(message) {
    this.messageCallbacks.forEach(callback => callback(message));
  }
  
  notifyUserStatus(status) {
    this.userStatusCallbacks.forEach(callback => callback(status));
  }
  
  notifyConnectionStatus(connected, reason) {
    this.connectionCallbacks.forEach(callback => callback(connected, reason));
  }
  
  notifyError(error) {
    this.errorCallbacks.forEach(callback => callback(error));
  }
  
  disconnect() {
    if (this.subscriptionId) {
      this.socket.send(JSON.stringify({
        type: 'unsubscribe',
        subscription_id: this.subscriptionId
      }));
    }
    
    this.socket.close(1000, 'User disconnected');
    this.connectionState = 'disconnected';
  }
}

// Usage example
const chatClient = new ChatClient('your_api_key_here', 'room_12345');

chatClient
  .onMessage(message => {
    console.log(`New message from ${message.user.name}: ${message.content}`);
    // Add message to UI
    addMessageToUI(message);
  })
  .onUserStatus(status => {
    console.log(`User ${status.user.name} is now ${status.status}`);
    // Update user status in UI
    updateUserStatusUI(status);
  });

// Send a message
document.getElementById('send-button').addEventListener('click', () => {
  const messageInput = document.getElementById('message-input');
  const message = messageInput.value.trim();
  
  if (message) {
    chatClient.sendMessage(message)
      .then(() => {
        messageInput.value = '';
      })
      .catch(error => {
        console.error('Error sending message:', error);
      });
  }
});

// Disconnect when leaving the page
window.addEventListener('beforeunload', () => {
  chatClient.disconnect();
});