Setup Zoom Websockets
OpenAIOfficialCreated by ZoomDistributed by OpenAI
- Skill
- Tool integration
- Automation and integrations
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a structured approach to integrating Zoom WebSockets for real-time event streaming. It covers the full connection lifecycle, including Server-to-Server OAuth authentication, token management, heartbeat implementation, and reconnection strategies. It is intended for developers who require persistent event delivery rather than standard HTTP webhook callbacks.
What it does
- Provides steps for configuring Zoom event subscriptions for WebSockets.
- Details the authentication flow using Server-to-Server OAuth tokens.
- Outlines implementation patterns for connection heartbeats and keep-alive pings.
- Defines strategies for handling disconnections and implementing exponential backoff.
- Lists required environment variables for managing Zoom app credentials and subscription IDs.
- Offers guidance on observability and debugging common connection issues.
When to use it
- When your application requires persistent event delivery from Zoom.
- When you need to manage real-time event streams outside of standard HTTP webhook patterns.
- When building custom integration logic that requires manual connection state management.
Advantages
- Provides a clear lifecycle model for managing persistent WebSocket connections.
- Includes code patterns for token refresh and heartbeat maintenance.
- Centralizes configuration requirements for Zoom OAuth and subscription IDs.
- Offers a structured approach to troubleshooting connection and authentication failures.
Usage examples
- “How do I authenticate a Zoom WebSocket connection?”
- The skill explains how to generate a Server-to-Server OAuth access token using your account ID, client ID, and client secret to authorize the WebSocket connection.
- “How can I keep my Zoom WebSocket connection alive?”
- The skill provides a heartbeat implementation pattern using periodic ping/pong messages to maintain the connection state.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/openai/setup-zoom-websockets/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip setup-zoom-websockets.zip -d .agents/skills
Documentation by Zoom
Setup Zoom WebSockets
Use this skill when the integration needs persistent Zoom event delivery instead of HTTP webhook callbacks. If normal webhook retries and delivery are enough, prefer setup-zoom-webhooks.
Workflow
Confirm WebSockets are justified by latency, firewall, connection model, or deployment constraints.
Configure the app and event subscriptions for the required event stream.
Implement connection setup, authentication, heartbeat, reconnect, backoff, and shutdown handling.
Normalize events into the same internal contract used by webhook handlers when both are supported.
Add observability for connection state, reconnect count, event lag, and dropped messages.
Debug by isolating token/auth problems, app subscription settings, network proxies, TLS interception, and reconnect loops.
References
Full preserved guide: references/full-guide.md
Connection: references/connection.md
Events: references/events.md
Common issues: troubleshooting/common-issues.md
Files
- ATTRIBUTION.mdAdded by AutoGineer878 B
- LICENSELicence file added by AutoGineer1.1 KiB
- RUNBOOK.md2.9 KiB
- SKILL.md1.2 KiB
- agents/openai.yaml148 B
- references/connection.md10.2 KiB
- references/environment-variables.md827 B
- references/events.md10.5 KiB
- references/full-guide.md7.0 KiB
- troubleshooting/common-issues.md1.1 KiB
references/connection.md
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 doneAuthentication
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
WebSockets docs: https://developers.zoom.us/docs/api/websockets/
S2S OAuth guide: https://developers.zoom.us/docs/internal-apps/s2s-oauth/
Requirements and compatibility
Written for: OpenAI Codex
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- Zoom
- Publisher
- OpenAI (Official)
- Distributed by
- OpenAI
- Source
- openai/plugins · plugins/zoom/skills/websockets · commit d420d13
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from plugins/zoom/LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Zoom or OpenAI.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download setup-zoom-websockets.zip