
Setup Zoom Websockets
- 1.4k installs
- 23.1k repo stars
- Updated July 28, 2026
- anthropics/knowledge-work-plugins
setup-zoom-websockets is an agent skill for reference skill for zoom websockets. use after routing to a low-latency event workflow when persistent connections, faster event delivery, or security constraints make.
About
The setup-zoom-websockets skill is designed for reference skill for Zoom WebSockets. Use after routing to a low-latency event workflow when persistent connections, faster event delivery, or security constraints make. /setup-zoom-websockets Background reference for persistent Zoom event streams. Prefer workflow routing first, then use this file when WebSockets are plausibly better than webhooks. Invoke when the user asks about setup zoom websockets or related SKILL.md workflows.
- Real-time, low-latency updates are critical.
- Security is paramount (banking, healthcare, finance).
- You don't want to expose a public endpoint.
- You need bidirectional communication.
- Simpler setup is preferred.
Setup Zoom Websockets by the numbers
- 1,412 all-time installs (skills.sh)
- +84 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #326 of 2,209 Security skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
setup-zoom-websockets capabilities & compatibility
- Capabilities
- real time, low latency updates are critical · security is paramount (banking, healthcare, fina · you don't want to expose a public endpoint · you need bidirectional communication
What setup-zoom-websockets says it does
Reference skill for Zoom WebSockets. Use after routing to a low-latency event workflow when persistent connections, faster event delivery, or security constraints make WebSockets p
Reference skill for Zoom WebSockets. Use after routing to a low-latency event workflow when persistent connections, faster event delivery, or security constrain
npx skills add https://github.com/anthropics/knowledge-work-plugins --skill setup-zoom-websocketsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.4k |
|---|---|
| repo stars | ★ 23.1k |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 28, 2026 |
| Repository | anthropics/knowledge-work-plugins ↗ |
How do I reference skill for zoom websockets. use after routing to a low-latency event workflow when persistent connections, faster event delivery, or security constraints make?
Reference skill for Zoom WebSockets. Use after routing to a low-latency event workflow when persistent connections, faster event delivery, or security constraints make.
Who is it for?
Developers using setup zoom websockets workflows documented in SKILL.md.
Skip if: Skip when the task falls outside setup-zoom-websockets scope or needs a different stack.
When should I use this skill?
User asks about setup zoom websockets or related SKILL.md workflows.
What you get
Completed setup-zoom-websockets workflow with documented commands, files, and expected deliverables.
- WebSocket client setup
- Token exchange function
By the numbers
- Documents a 5-step WebSocket connection lifecycle
Files
/setup-zoom-websockets
Background reference for persistent Zoom event streams. Prefer workflow routing first, then use this file when WebSockets are plausibly better than webhooks.
WebSockets vs Webhooks
| 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:
- Real-time, low-latency updates are critical
- Security is paramount (banking, healthcare, finance)
- You don't want to expose a public endpoint
- You need bidirectional communication
Choose Webhooks when:
- Simpler setup is preferred
- Small number of event notifications
- Existing HTTP infrastructure
Prerequisites
- Server-to-Server OAuth app in Zoom Marketplace
- Account ID, Client ID, and Client Secret
- WebSocket subscription with events enabled
Need help with S2S OAuth? See the [zoom-oauth](../oauth/SKILL.md) skill for complete authentication flows.
Start troubleshooting fast: Use the [5-Minute Runbook](RUNBOOK.md) before deep debugging.
Quick Start
1. Create Server-to-Server OAuth App
1. Go to Zoom Marketplace 2. Create a Server-to-Server OAuth app 3. Copy Account ID, Client ID, Client Secret
2. Enable WebSocket Subscription
1. In your app, go to Feature → Event Subscriptions 2. Add an Event Subscription 3. Select WebSockets as the method type 4. Select events to subscribe to (e.g., meeting.created, meeting.started) 5. Save - an endpoint URL will be generated
3. Connect via WebSocket
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();Event Format
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"
}
}
}Common Events
| 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 |
Connection Management
Keep-Alive
WebSocket connections require periodic heartbeats. Zoom will close idle connections.
// Send ping every 30 seconds
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.ping();
}
}, 30000);Reconnection
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;
}Single Connection Limit
Important: Only ONE WebSocket connection can be open per subscription at a time. Opening a new connection will close the existing one.
Detailed References
- [references/connection.md](references/connection.md) - Connection lifecycle, authentication, error handling
- [references/events.md](references/events.md) - Complete event types reference
Troubleshooting
- [troubleshooting/common-issues.md](troubleshooting/common-issues.md) - Subscription URL confusion, disconnects, no-events debugging
Sample Repositories
Official / Community
| Type | Repository | Description |
|---|---|---|
| Node.js | just-zoomit/zoom-websockets | WebSocket sample with S2S OAuth |
WebSockets vs RTMS
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.
Resources
- WebSockets docs: https://developers.zoom.us/docs/api/websockets/
- Webhooks comparison: https://www.zoom.com/en/blog/a-guide-to-webhooks-and-websockets/
- Developer forum: https://devforum.zoom.us/
Environment Variables
- See references/environment-variables.md for standardized
.envkeys and where to find each value.
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/
Zoom WebSockets Environment Variables
Standard .env keys
| Variable | Required | Used for | Where to find |
|---|---|---|---|
ZOOM_CLIENT_ID | Yes | OAuth app identity for WebSocket subscriptions | Zoom Marketplace -> OAuth app -> App Credentials |
ZOOM_CLIENT_SECRET | Yes | OAuth app secret | Zoom Marketplace -> OAuth app -> App Credentials |
ZOOM_ACCOUNT_ID | S2S OAuth mode | Account-level token grant for service apps | Zoom Marketplace -> Server-to-Server OAuth app credentials |
ZOOM_SUBSCRIPTION_ID | After setup | Persisted subscription identifier for reconnect/resume | Returned by subscription create API response |
Runtime-only values
ZOOM_ACCESS_TOKEN
Notes
ZOOM_SUBSCRIPTION_IDis not from Marketplace UI; your app stores it after calling the subscription API.
WebSockets - Event Types
Complete reference for events available via Zoom WebSockets.
Event Structure
All WebSocket events follow this structure:
{
"event": "event.type",
"event_ts": 1706123456789,
"payload": {
"account_id": "abcD3ojkdbjfg",
"object": {
// Event-specific data
}
}
}| Field | Type | Description |
|---|---|---|
event | string | Event type identifier |
event_ts | number | Unix timestamp (milliseconds) |
payload.account_id | string | Zoom account ID |
payload.object | object | Event-specific payload |
Meeting Events
meeting.created
Triggered when a meeting is scheduled.
{
"event": "meeting.created",
"payload": {
"account_id": "abcD3ojkdbjfg",
"object": {
"id": 1234567890,
"uuid": "abcdefgh-1234-5678-abcd-1234567890ab",
"host_id": "xyz789",
"topic": "Weekly Team Sync",
"type": 2,
"start_time": "2024-01-25T10:00:00Z",
"duration": 60,
"timezone": "America/Los_Angeles"
}
}
}meeting.updated
Triggered when meeting settings are changed.
{
"event": "meeting.updated",
"payload": {
"account_id": "abcD3ojkdbjfg",
"object": {
"id": 1234567890,
"topic": "Updated: Weekly Team Sync"
},
"old_object": {
"topic": "Weekly Team Sync"
}
}
}meeting.deleted
Triggered when a meeting is deleted.
{
"event": "meeting.deleted",
"payload": {
"account_id": "abcD3ojkdbjfg",
"object": {
"id": 1234567890,
"uuid": "abcdefgh-1234-5678-abcd-1234567890ab",
"host_id": "xyz789"
}
}
}meeting.started
Triggered when a meeting begins.
{
"event": "meeting.started",
"payload": {
"account_id": "abcD3ojkdbjfg",
"object": {
"id": 1234567890,
"uuid": "abcdefgh-1234-5678-abcd-1234567890ab",
"host_id": "xyz789",
"topic": "Weekly Team Sync",
"type": 2,
"start_time": "2024-01-25T10:00:00Z",
"timezone": "America/Los_Angeles"
}
}
}meeting.ended
Triggered when a meeting ends.
{
"event": "meeting.ended",
"payload": {
"account_id": "abcD3ojkdbjfg",
"object": {
"id": 1234567890,
"uuid": "abcdefgh-1234-5678-abcd-1234567890ab",
"host_id": "xyz789",
"topic": "Weekly Team Sync",
"start_time": "2024-01-25T10:00:00Z",
"end_time": "2024-01-25T11:05:00Z",
"duration": 65
}
}
}meeting.participant_joined
Triggered when a participant joins the meeting.
{
"event": "meeting.participant_joined",
"payload": {
"account_id": "abcD3ojkdbjfg",
"object": {
"id": 1234567890,
"uuid": "abcdefgh-1234-5678-abcd-1234567890ab",
"host_id": "xyz789",
"participant": {
"id": "participant123",
"user_id": "user456",
"user_name": "John Doe",
"email": "john@example.com",
"join_time": "2024-01-25T10:02:00Z"
}
}
}
}meeting.participant_left
Triggered when a participant leaves the meeting.
{
"event": "meeting.participant_left",
"payload": {
"account_id": "abcD3ojkdbjfg",
"object": {
"id": 1234567890,
"uuid": "abcdefgh-1234-5678-abcd-1234567890ab",
"participant": {
"id": "participant123",
"user_name": "John Doe",
"leave_time": "2024-01-25T10:45:00Z",
"leave_reason": "left the meeting"
}
}
}
}meeting.sharing_started
Triggered when screen sharing begins.
{
"event": "meeting.sharing_started",
"payload": {
"account_id": "abcD3ojkdbjfg",
"object": {
"id": 1234567890,
"uuid": "abcdefgh-1234-5678-abcd-1234567890ab",
"participant": {
"id": "participant123",
"user_name": "John Doe"
},
"sharing_details": {
"content": "screen",
"date_time": "2024-01-25T10:15:00Z"
}
}
}
}meeting.sharing_ended
Triggered when screen sharing ends.
{
"event": "meeting.sharing_ended",
"payload": {
"account_id": "abcD3ojkdbjfg",
"object": {
"id": 1234567890,
"uuid": "abcdefgh-1234-5678-abcd-1234567890ab",
"participant": {
"id": "participant123",
"user_name": "John Doe"
}
}
}
}Recording Events
recording.started
Triggered when cloud recording starts.
{
"event": "recording.started",
"payload": {
"account_id": "abcD3ojkdbjfg",
"object": {
"id": 1234567890,
"uuid": "abcdefgh-1234-5678-abcd-1234567890ab",
"host_id": "xyz789",
"topic": "Weekly Team Sync",
"start_time": "2024-01-25T10:00:00Z",
"recording_start": "2024-01-25T10:01:00Z"
}
}
}recording.stopped
Triggered when cloud recording stops (paused or ended).
{
"event": "recording.stopped",
"payload": {
"account_id": "abcD3ojkdbjfg",
"object": {
"id": 1234567890,
"uuid": "abcdefgh-1234-5678-abcd-1234567890ab",
"recording_start": "2024-01-25T10:01:00Z",
"recording_end": "2024-01-25T11:00:00Z"
}
}
}recording.completed
Triggered when cloud recording is processed and ready for download.
{
"event": "recording.completed",
"payload": {
"account_id": "abcD3ojkdbjfg",
"object": {
"id": 1234567890,
"uuid": "abcdefgh-1234-5678-abcd-1234567890ab",
"host_id": "xyz789",
"topic": "Weekly Team Sync",
"start_time": "2024-01-25T10:00:00Z",
"duration": 60,
"total_size": 157286400,
"recording_count": 2,
"recording_files": [
{
"id": "file123",
"meeting_id": "abcdefgh-1234-5678-abcd-1234567890ab",
"recording_start": "2024-01-25T10:01:00Z",
"recording_end": "2024-01-25T11:00:00Z",
"file_type": "MP4",
"file_size": 104857600,
"download_url": "https://zoom.us/rec/download/...",
"status": "completed"
},
{
"id": "file124",
"file_type": "TRANSCRIPT",
"file_size": 52428800,
"download_url": "https://zoom.us/rec/download/..."
}
]
}
}
}recording.trashed
Triggered when recording is moved to trash.
recording.deleted
Triggered when recording is permanently deleted.
recording.recovered
Triggered when recording is restored from trash.
User Events
user.created
Triggered when a new user is added to the account.
{
"event": "user.created",
"payload": {
"account_id": "abcD3ojkdbjfg",
"object": {
"id": "user789",
"first_name": "Jane",
"last_name": "Smith",
"email": "jane.smith@example.com",
"type": 2,
"created_at": "2024-01-25T09:00:00Z"
}
}
}user.updated
Triggered when user details are changed.
{
"event": "user.updated",
"payload": {
"account_id": "abcD3ojkdbjfg",
"object": {
"id": "user789",
"first_name": "Jane",
"last_name": "Smith-Jones"
},
"old_object": {
"last_name": "Smith"
}
}
}user.deleted
Triggered when a user is removed from the account.
{
"event": "user.deleted",
"payload": {
"account_id": "abcD3ojkdbjfg",
"object": {
"id": "user789",
"email": "jane.smith@example.com"
}
}
}user.deactivated
Triggered when a user is deactivated.
user.activated
Triggered when a user is activated.
Webinar Events
webinar.created
webinar.updated
webinar.deleted
webinar.started
webinar.ended
webinar.registration_created
Triggered when someone registers for a webinar.
{
"event": "webinar.registration_created",
"payload": {
"account_id": "abcD3ojkdbjfg",
"object": {
"id": 9876543210,
"uuid": "webinar-uuid-here",
"registrant": {
"id": "registrant123",
"email": "attendee@example.com",
"first_name": "Attendee",
"last_name": "User",
"join_url": "https://zoom.us/w/..."
}
}
}
}Event Handling Example
const eventHandlers = {
// Meeting events
'meeting.created': (payload) => {
console.log(`New meeting: ${payload.object.topic}`);
notifyCalendarService(payload.object);
},
'meeting.started': (payload) => {
console.log(`Meeting started: ${payload.object.topic}`);
updateMeetingStatus(payload.object.id, 'in_progress');
},
'meeting.ended': (payload) => {
console.log(`Meeting ended: ${payload.object.uuid}`);
updateMeetingStatus(payload.object.id, 'completed');
calculateAttendance(payload.object);
},
'meeting.participant_joined': (payload) => {
const { participant } = payload.object;
console.log(`${participant.user_name} joined`);
trackAttendance(payload.object.id, participant);
},
// Recording events
'recording.completed': (payload) => {
console.log(`Recording ready: ${payload.object.topic}`);
downloadRecordings(payload.object.recording_files);
},
// User events
'user.created': (payload) => {
console.log(`New user: ${payload.object.email}`);
sendWelcomeEmail(payload.object);
}
};
ws.on('message', (data) => {
const event = JSON.parse(data);
const handler = eventHandlers[event.event];
if (handler) {
handler(event.payload);
} else {
console.log(`Unhandled event: ${event.event}`);
}
});Subscribing to Events
Configure which events to receive in your Zoom Marketplace app:
1. Go to Feature → Event Subscriptions 2. Select WebSockets as method type 3. Check the events you want to receive 4. Save the subscription
Note: You can modify subscriptions at any time. Changes take effect immediately.
Event Filtering
If you're receiving too many events, consider:
1. Subscribe selectively - Only subscribe to events you need 2. Filter in handler - Drop events that don't match your criteria 3. Use multiple subscriptions - Route different events to different handlers
// Filter example: Only process events for specific hosts
ws.on('message', (data) => {
const event = JSON.parse(data);
// Only process meetings from specific hosts
const allowedHosts = ['host1@example.com', 'host2@example.com'];
if (event.event.startsWith('meeting.') &&
event.payload.object.host_id &&
!allowedHosts.includes(getHostEmail(event.payload.object.host_id))) {
return; // Skip this event
}
processEvent(event);
});Resources
- Event reference: https://developers.zoom.us/docs/api/rest/reference/zoom-api/events/
- WebSockets docs: https://developers.zoom.us/docs/api/websockets/
WebSockets 5-Minute Preflight Runbook
Use this before deep debugging. It catches common Zoom WebSockets failures quickly.
Skill Doc Standard Note
- Agent-skill standard entrypoint is
SKILL.md. - This runbook is an operational convention (recommended), not a required skill file.
SKILL.mdis also a navigation convention for larger skill docs.
1) Confirm OAuth Token Generation
- Use S2S credentials and account ID.
- Token endpoint:
https://zoom.us/oauth/token. - Refresh token before expiry.
Token Sanity Checks
- Verify token response is JSON and contains
access_token. - Record token expiry and refresh proactively.
- If auth intermittently fails, check for clock skew and stale cached tokens.
2) Confirm Subscription Configuration
- Event subscription created with WebSockets delivery type.
- Required event types selected and saved.
3) Confirm Connection URL and Auth
- Use exact WebSocket URL from Zoom subscription config.
- Attach access token as required by protocol/headers.
4) Confirm Runtime Reliability
- Implement reconnect with backoff.
- Handle heartbeat/ping-pong and connection lifecycle events.
- Prevent duplicate consumers if multiple workers run.
Minimal Reliability Policy
- Backoff: exponential with jitter.
- Cap retries and alert after sustained failures.
- Ensure only one active consumer per subscription stream in each environment.
5) Confirm Event Processing Semantics
- Handle ordering assumptions carefully.
- Make event handlers idempotent.
- Log event IDs and delivery timestamps.
6) Quick Probes
- Access token request succeeds and returns JSON.
- WebSocket connects and receives at least one subscribed event.
- Reconnect path works after forced disconnect.
Copy/Paste Validation Commands
# 1) Validate S2S token request
curl -X POST "https://zoom.us/oauth/token" \
-H "Authorization: Basic $(printf '%s:%s' "$ZOOM_CLIENT_ID" "$ZOOM_CLIENT_SECRET" | base64)" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=account_credentials&account_id=$ZOOM_ACCOUNT_ID"
# 2) Basic Zoom API probe with token
curl -X GET "https://api.zoom.us/v2/users/me" \
-H "Authorization: Bearer $ZOOM_ACCESS_TOKEN"
# 3) Tail app logs while forcing reconnect tests
pm2 logs your-websocket-service --lines 120Expected: token/API probes return JSON; websocket service logs show connect -> receive -> reconnect sequence.
7) Fast Decision Tree
- Connection refused/closed -> token invalid, wrong URL, or subscription config issue.
- Connected but no events -> wrong event selection or no triggering activity.
- Event storms/duplicates -> missing dedupe/idempotency logic.
8) WebSockets vs Webhooks Guardrail
- If your use case does not need persistent low-latency delivery, webhook delivery may be simpler to operate.
- Choose WebSockets when you can own connection lifecycle monitoring and reconnect behavior.
Common Issues
Quick diagnostics for Zoom WebSockets event subscriptions.
"Where Is the WebSocket URL?"
Symptom: You can’t find a generic wss://... endpoint that works for everyone.
Reality: Your connection is parameterized by your subscription (subscriptionId) and an access token.
See: connection.md
Disconnects / Reconnect Loops
Common causes:
- Access token expired (typically ~1 hour).
- Single-connection limit per subscription (a new connection may close the previous one).
- No heartbeat/keep-alive handling in your client.
Fix:
- Refresh token proactively and reconnect with the new token.
- Implement exponential backoff (with jitter).
- Ensure only one active connection per subscription.
No Events Received
Common causes:
- Subscribed event topics don’t match what you’re testing.
- App/subscription not enabled or not deployed as required by your account settings.
Fix:
- Confirm topics in Marketplace and generate an event you actually subscribed to.
- Log raw incoming messages and validate parsing.
Related skills
FAQ
What does setup-zoom-websockets do?
Reference skill for Zoom WebSockets. Use after routing to a low-latency event workflow when persistent connections, faster event delivery, or security constraints make.
When should I use setup-zoom-websockets?
User asks about setup zoom websockets or related SKILL.md workflows.
Is setup-zoom-websockets safe to install?
Review the Security Audits panel on this page before installing in production.