Files
2026-07-13 12:20:06 +08:00

10 KiB

WebSockets - Connection Management

Detailed guide for managing WebSocket connections to Zoom.

Connection Lifecycle

1. Generate access token (S2S OAuth)
         ↓
2. Open WebSocket connection with token
         ↓
3. Receive events in real-time
         ↓
4. Handle disconnects and reconnect
         ↓
5. Close connection when done

Authentication

WebSocket connections require a valid Server-to-Server OAuth access token.

Generate Access Token

const axios = require('axios');

async function getAccessToken(accountId, clientId, clientSecret) {
  const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
  
  const response = await axios.post(
    'https://zoom.us/oauth/token',
    new URLSearchParams({
      grant_type: 'account_credentials',
      account_id: accountId
    }),
    {
      headers: {
        'Authorization': `Basic ${credentials}`,
        'Content-Type': 'application/x-www-form-urlencoded'
      }
    }
  );
  
  return {
    accessToken: response.data.access_token,
    expiresIn: response.data.expires_in // Usually 3600 seconds (1 hour)
  };
}

Token Refresh

Access tokens expire after 1 hour. Implement token refresh before expiration:

class ZoomWebSocketClient {
  constructor(accountId, clientId, clientSecret, subscriptionId) {
    this.accountId = accountId;
    this.clientId = clientId;
    this.clientSecret = clientSecret;
    this.subscriptionId = subscriptionId;
    this.ws = null;
    this.tokenExpiry = null;
  }
  
  async refreshTokenIfNeeded() {
    const now = Date.now();
    const bufferTime = 5 * 60 * 1000; // 5 minutes before expiry
    
    if (!this.tokenExpiry || now >= this.tokenExpiry - bufferTime) {
      const { accessToken, expiresIn } = await getAccessToken(
        this.accountId, this.clientId, this.clientSecret
      );
      this.accessToken = accessToken;
      this.tokenExpiry = now + (expiresIn * 1000);
      
      // Reconnect with new token
      if (this.ws) {
        this.ws.close();
        await this.connect();
      }
    }
  }
  
  async connect() {
    await this.refreshTokenIfNeeded();
    
    const wsUrl = `wss://ws.zoom.us/ws?subscriptionId=${this.subscriptionId}&access_token=${this.accessToken}`;
    this.ws = new WebSocket(wsUrl);
    
    // Set up event handlers...
  }
}

Connection URL

wss://ws.zoom.us/ws?subscriptionId={SUBSCRIPTION_ID}&access_token={ACCESS_TOKEN}
Parameter Description
subscriptionId Your WebSocket subscription ID from Marketplace
access_token Valid S2S OAuth access token

Connection Limits

Limit Value
Connections per subscription 1 (opening new connection closes existing)
Connection timeout Varies (implement keep-alive)
Message size Check Zoom docs for current limits

Keep-Alive / Heartbeat

Maintain connection with periodic pings:

class WebSocketManager {
  constructor() {
    this.ws = null;
    this.pingInterval = null;
  }
  
  startHeartbeat() {
    // Ping every 30 seconds
    this.pingInterval = setInterval(() => {
      if (this.ws && this.ws.readyState === WebSocket.OPEN) {
        this.ws.ping();
        console.log('Ping sent');
      }
    }, 30000);
  }
  
  stopHeartbeat() {
    if (this.pingInterval) {
      clearInterval(this.pingInterval);
      this.pingInterval = null;
    }
  }
  
  connect(url) {
    this.ws = new WebSocket(url);
    
    this.ws.on('open', () => {
      console.log('Connected');
      this.startHeartbeat();
    });
    
    this.ws.on('pong', () => {
      console.log('Pong received - connection alive');
    });
    
    this.ws.on('close', () => {
      this.stopHeartbeat();
    });
  }
}

Reconnection Strategy

Implement exponential backoff for reconnection:

class ReconnectingWebSocket {
  constructor(config) {
    this.config = config;
    this.ws = null;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = 10;
    this.baseDelay = 1000; // 1 second
    this.maxDelay = 30000; // 30 seconds
  }
  
  async connect() {
    try {
      const token = await getAccessToken(
        this.config.accountId,
        this.config.clientId,
        this.config.clientSecret
      );
      
      const url = `wss://ws.zoom.us/ws?subscriptionId=${this.config.subscriptionId}&access_token=${token.accessToken}`;
      
      this.ws = new WebSocket(url);
      
      this.ws.on('open', () => {
        console.log('Connected successfully');
        this.reconnectAttempts = 0; // Reset on successful connection
      });
      
      this.ws.on('close', (code, reason) => {
        console.log(`Disconnected: ${code} - ${reason}`);
        this.scheduleReconnect();
      });
      
      this.ws.on('error', (error) => {
        console.error('WebSocket error:', error.message);
      });
      
      this.ws.on('message', (data) => {
        this.handleMessage(JSON.parse(data));
      });
      
    } catch (error) {
      console.error('Connection failed:', error.message);
      this.scheduleReconnect();
    }
  }
  
  scheduleReconnect() {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.error('Max reconnection attempts reached');
      return;
    }
    
    // Exponential backoff with jitter
    const delay = Math.min(
      this.baseDelay * Math.pow(2, this.reconnectAttempts) + Math.random() * 1000,
      this.maxDelay
    );
    
    console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1})`);
    
    setTimeout(() => {
      this.reconnectAttempts++;
      this.connect();
    }, delay);
  }
  
  handleMessage(event) {
    // Override this method to handle events
    console.log('Event:', event.event, event.payload);
  }
  
  close() {
    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }
  }
}

Error Handling

Common Error Codes

Code Meaning Action
1000 Normal closure Clean shutdown
1001 Going away Server shutting down, reconnect
1006 Abnormal closure Network issue, reconnect
1008 Policy violation Check token validity
1011 Internal error Server error, retry later

Error Handling Example

ws.on('close', (code, reason) => {
  switch (code) {
    case 1000:
      console.log('Connection closed normally');
      break;
    case 1001:
    case 1006:
      console.log('Connection lost, reconnecting...');
      scheduleReconnect();
      break;
    case 1008:
      console.log('Auth error - refreshing token');
      refreshTokenAndReconnect();
      break;
    default:
      console.log(`Unexpected close: ${code} - ${reason}`);
      scheduleReconnect();
  }
});

ws.on('error', (error) => {
  console.error('WebSocket error:', error);
  // The 'close' event will follow, handle reconnection there
});

Complete Example

const WebSocket = require('ws');
const axios = require('axios');

class ZoomWebSocketClient {
  constructor(config) {
    this.config = config;
    this.ws = null;
    this.accessToken = null;
    this.tokenExpiry = null;
    this.pingInterval = null;
    this.reconnectAttempts = 0;
    this.handlers = new Map();
  }
  
  on(event, handler) {
    this.handlers.set(event, handler);
  }
  
  async getAccessToken() {
    const credentials = Buffer.from(
      `${this.config.clientId}:${this.config.clientSecret}`
    ).toString('base64');
    
    const response = await axios.post(
      'https://zoom.us/oauth/token',
      new URLSearchParams({
        grant_type: 'account_credentials',
        account_id: this.config.accountId
      }),
      {
        headers: {
          'Authorization': `Basic ${credentials}`,
          'Content-Type': 'application/x-www-form-urlencoded'
        }
      }
    );
    
    this.accessToken = response.data.access_token;
    this.tokenExpiry = Date.now() + (response.data.expires_in * 1000);
    
    return this.accessToken;
  }
  
  async connect() {
    await this.getAccessToken();
    
    const url = `wss://ws.zoom.us/ws?subscriptionId=${this.config.subscriptionId}&access_token=${this.accessToken}`;
    
    this.ws = new WebSocket(url);
    
    this.ws.on('open', () => {
      console.log('WebSocket connected');
      this.reconnectAttempts = 0;
      this.startPing();
      this.scheduleTokenRefresh();
    });
    
    this.ws.on('message', (data) => {
      const event = JSON.parse(data);
      const handler = this.handlers.get(event.event);
      if (handler) {
        handler(event.payload);
      }
    });
    
    this.ws.on('close', (code, reason) => {
      console.log(`Disconnected: ${code}`);
      this.stopPing();
      if (code !== 1000) {
        this.reconnect();
      }
    });
    
    this.ws.on('error', (error) => {
      console.error('Error:', error.message);
    });
  }
  
  startPing() {
    this.pingInterval = setInterval(() => {
      if (this.ws?.readyState === WebSocket.OPEN) {
        this.ws.ping();
      }
    }, 30000);
  }
  
  stopPing() {
    if (this.pingInterval) {
      clearInterval(this.pingInterval);
    }
  }
  
  scheduleTokenRefresh() {
    const refreshIn = this.tokenExpiry - Date.now() - 300000; // 5 min before expiry
    setTimeout(() => this.refreshToken(), refreshIn);
  }
  
  async refreshToken() {
    await this.getAccessToken();
    // Close and reconnect with new token
    this.ws?.close(1000);
    await this.connect();
  }
  
  reconnect() {
    const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
    this.reconnectAttempts++;
    console.log(`Reconnecting in ${delay}ms...`);
    setTimeout(() => this.connect(), delay);
  }
  
  disconnect() {
    this.stopPing();
    this.ws?.close(1000);
  }
}

// Usage
const client = new ZoomWebSocketClient({
  accountId: process.env.ZOOM_ACCOUNT_ID,
  clientId: process.env.ZOOM_CLIENT_ID,
  clientSecret: process.env.ZOOM_CLIENT_SECRET,
  subscriptionId: process.env.ZOOM_SUBSCRIPTION_ID
});

client.on('meeting.started', (payload) => {
  console.log(`Meeting started: ${payload.object.topic}`);
});

client.on('meeting.ended', (payload) => {
  console.log(`Meeting ended: ${payload.object.uuid}`);
});

client.on('meeting.participant_joined', (payload) => {
  console.log(`Participant joined: ${payload.object.participant.user_name}`);
});

client.connect();

Resources