From zoom-skills
Establishes persistent Zoom WebSocket connections for low-latency, bidirectional real-time event notifications. Alternative to webhooks for security-sensitive apps and faster delivery.
npx claudepluginhub zoom/skills --plugin zoom-skillsThis skill uses the workspace's default tool permissions.
Receive real-time event notifications from Zoom via persistent WebSocket connection.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Provides Docker Compose setups for local dev stacks with databases and caching, multi-stage Dockerfiles for dev/prod, and patterns for security, networking, volumes, healthchecks.
Receive real-time event notifications from Zoom via persistent WebSocket connection.
| Aspect | WebSockets | Webhooks |
|---|---|---|
| Connection | Persistent, bidirectional | One-time HTTP POST |
| Latency | Lower (no HTTP overhead) | Higher (new connection per event) |
| Security | Direct connection, no exposed endpoint | Requires endpoint validation, IP whitelisting |
| Model | Pull (you connect to Zoom) | Push (Zoom connects to you) |
| State | Stateful (maintains connection) | Stateless (each event independent) |
| Setup | More complex (access token, connection) | Simpler (just endpoint URL) |
Choose WebSockets when:
Choose Webhooks when:
Need help with S2S OAuth? See the zoom-oauth skill for complete authentication flows.
Start troubleshooting fast: Use the 5-Minute Runbook before deep debugging.
meeting.created, meeting.started)const WebSocket = require('ws');
const axios = require('axios');
// Step 1: Get access token
async function getAccessToken() {
const credentials = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64');
const response = await axios.post(
'https://zoom.us/oauth/token',
new URLSearchParams({
grant_type: 'account_credentials',
account_id: ACCOUNT_ID
}),
{
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
return response.data.access_token;
}
// Step 2: Connect to WebSocket
async function connectWebSocket() {
const accessToken = await getAccessToken();
// WebSocket URL from your subscription settings
const wsUrl = `wss://ws.zoom.us/ws?subscriptionId=${SUBSCRIPTION_ID}&access_token=${accessToken}`;
const ws = new WebSocket(wsUrl);
ws.on('open', () => {
console.log('WebSocket connection established');
});
ws.on('message', (data) => {
const event = JSON.parse(data);
console.log('Event received:', event.event);
// Handle different event types
switch (event.event) {
case 'meeting.started':
console.log(`Meeting started: ${event.payload.object.topic}`);
break;
case 'meeting.ended':
console.log(`Meeting ended: ${event.payload.object.uuid}`);
break;
case 'meeting.participant_joined':
console.log(`Participant joined: ${event.payload.object.participant.user_name}`);
break;
}
});
ws.on('close', (code, reason) => {
console.log(`Connection closed: ${code} - ${reason}`);
// Implement reconnection logic
});
ws.on('error', (error) => {
console.error('WebSocket error:', error);
});
return ws;
}
connectWebSocket();
Events received via WebSocket have the same format as webhook events:
{
"event": "meeting.started",
"event_ts": 1706123456789,
"payload": {
"account_id": "abcD3ojkdbjfg",
"object": {
"id": 1234567890,
"uuid": "abcdefgh-1234-5678-abcd-1234567890ab",
"host_id": "xyz789",
"topic": "Team Standup",
"type": 2,
"start_time": "2024-01-25T10:00:00Z",
"timezone": "America/Los_Angeles"
}
}
}
| Event | Description |
|---|---|
meeting.created | Meeting scheduled |
meeting.updated | Meeting settings changed |
meeting.deleted | Meeting deleted |
meeting.started | Meeting begins |
meeting.ended | Meeting ends |
meeting.participant_joined | Participant joins meeting |
meeting.participant_left | Participant leaves meeting |
recording.completed | Cloud recording ready |
user.created | New user added |
user.updated | User details changed |
WebSocket connections require periodic heartbeats. Zoom will close idle connections.
// Send ping every 30 seconds
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.ping();
}
}, 30000);
Implement automatic reconnection for reliability:
function connectWithReconnect() {
const ws = connectWebSocket();
ws.on('close', () => {
console.log('Connection lost. Reconnecting in 5 seconds...');
setTimeout(connectWithReconnect, 5000);
});
return ws;
}
Important: Only ONE WebSocket connection can be open per subscription at a time. Opening a new connection will close the existing one.
| Type | Repository | Description |
|---|---|---|
| Node.js | just-zoomit/zoom-websockets | WebSocket sample with S2S OAuth |
Don't confuse WebSockets with RTMS (Realtime Media Streams):
| Feature | WebSockets | RTMS |
|---|---|---|
| Purpose | Event notifications | Media streams |
| Data | Meeting events, user events | Audio, video, transcripts |
| Use case | React to Zoom events | AI/ML, live transcription |
| Skill | This skill | rtms |
For real-time audio/video/transcript data, use the rtms skill instead.
.env keys and where to find each value.