Generated banner for Zoom General
Generated logo for Zoom General

Zoom General

OpenAIOfficialCreated by ZoomDistributed by OpenAI

  • Skill
  • Tool integration
  • Automation and integrations
  • MIT
  • Medium risk

From the author

Use when comparing products.
— the description by Zoom

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

Zoom General serves as a strategic starting point for developers and architects to navigate the Zoom ecosystem. It provides a structured workflow to classify integration requirements, identify the correct Zoom product surface (such as APIs, SDKs, or webhooks), and determine the necessary authentication model. By guiding users through a decision-making process, it ensures that developers select the right tool for their specific use case before routing to more specialized skills.

What it does

  • Classifies integration jobs based on desired outcomes like embedding meetings or automating resources.
  • Maps requirements to specific Zoom surfaces including REST APIs, WebSockets, and various SDKs.
  • Provides guidance on selecting the correct authentication and scope model for an integration.
  • Facilitates routing to specialized skills once the product context is established.
  • Offers comparative context for tasks that span multiple Zoom product boundaries.

When to use it

  • When you are in the early stages of planning a Zoom integration and need to choose the right app type.
  • When you need to compare different Zoom surfaces to determine which fits your technical requirements.
  • When a task involves multiple Zoom products and requires a high-level architectural approach.
  • When you need to confirm the correct authentication flow for a specific integration scenario.

Advantages

  • Prevents premature selection of tools by enforcing a structured classification process.
  • Clarifies the distinction between user-level OAuth, account-level OAuth, and SDK signatures.
  • Reduces complexity by routing users to narrow, specialized skills after initial planning.
  • Provides a clear decision matrix for choosing between General Apps, Server-to-Server OAuth, and Webhook Only apps.

Usage examples

“I want to build an app that automates meeting reports. Which Zoom app type should I use?”
The agent will guide you to classify the job as backend automation, leading to the recommendation of Server-to-Server OAuth for its lack of user interaction requirements.
“Should I use the Meeting SDK or the Video SDK for my project?”
The agent will help you compare the two based on your specific embedding needs and surface requirements, ensuring you choose the correct SDK for your use case.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/openai/zoom-general/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip zoom-general.zip -d .agents/skills

Documentation by Zoom

Zoom General

Use this skill for cross-product platform context after the user’s goal is understood. For first-step routing, prefer start, plan-zoom-product, or plan-zoom-integration.

Workflow

  1. Classify the job by outcome: embed meetings, custom video, automate resources, consume events, process media, use meeting intelligence, or publish a Marketplace app.

  2. Choose the Zoom surface that owns the behavior: REST API, webhooks, WebSockets, Meeting SDK, Video SDK, Zoom Apps SDK, Phone, Contact Center, Virtual Agent, Scribe, Rivet, or Cobrowse.

  3. Confirm auth and scope model before implementation; Zoom surfaces differ on user-level OAuth, account-level OAuth, and SDK signatures.

  4. Route to the narrow skill once the surface is chosen rather than keeping broad guidance in context.

  5. Use the preserved guide only when a task crosses product boundaries or needs detailed comparison tables.

References

Files

use-cases/retrieve-meeting-and-subscribe-events.md

Retrieve Meeting Details and Subscribe to Events

Comprehensive guide showing all ways to retrieve meeting details and subscribe to events in Zoom. The term "subscribe to events" has multiple meanings in Zoom, each requiring different skill combinations.

Overview

When someone says "retrieve meeting details and subscribe to events," they could mean:

PatternEvent TypeSkillsWhen to Use
Pattern 1Server-side webhooks (HTTP)zoom-rest-api → webhooksAccount-level event monitoring, backend processing
Pattern 2Server-side WebSockets (WS)zoom-rest-api → zoom-websocketsLow-latency events, no exposed endpoint
Pattern 3Client-side SDK eventszoom-rest-api → zoom-meeting-sdkIn-meeting participant events, real-time UI updates
Pattern 4Real-time media eventszoom-rest-api → rtmsBot-based transcription, recording, AI analysis
Pattern 5Reports API (polling)zoom-rest-api onlyHistorical data, batch processing

Critical Distinction: Event Subscription Scope

Important: Most Zoom event subscriptions are account-level, not meeting-specific:

Subscription TypeScopeFiltering
WebhooksAccount-levelFilter events by meeting ID in handler
WebSocketsAccount-levelFilter events by meeting ID in handler
Meeting SDKMeeting-specificOnly events for joined meeting
RTMSMeeting-specificOnly events/media for specific meeting
Reports APIAccount-levelQuery by meeting ID

You cannot dynamically subscribe to webhooks/WebSockets for a single meeting. These are configured at app creation time in the Marketplace portal and receive events for ALL meetings in the account. Your code filters events by meeting ID.


Pattern 1: REST API + Webhooks (Server-Side HTTP Events)

When to use:

  • Backend processing of meeting lifecycle events

  • Attendance tracking, post-meeting workflows

  • Recording download automation

  • Standard event-driven architectures

Skills needed: zoom-rest-api → webhooks

Flow Diagram
┌────────────────────────────────────────────────────────────────┐
│ SETUP PHASE (One-time in Marketplace Portal):                  │
│                                                                 │
│ 1. Configure Event Subscriptions                               │
│    └── Subscribe to: meeting.started, meeting.ended, etc.      │
│    └── Provide webhook endpoint URL                            │
│    └── Get webhook secret token                                │
└────────────────────────────────────────────────────────────────┘
                             │
                             ▼
┌────────────────────────────────────────────────────────────────┐
│ RUNTIME PHASE:                                                  │
│                                                                 │
│ Step 1: GET /meetings/{meetingId} (zoom-rest-api)              │
│         └── Get meeting details (topic, join_url, host, etc.)  │
│                                                                 │
│ Step 2: Store meeting ID for filtering                         │
│         └── Your backend remembers which meetings to track     │
│                                                                 │
│ Step 3: POST /webhooks/zoom (webhooks)               │
│         └── Zoom sends events for ALL meetings                 │
│         └── Filter by meeting ID in your handler               │
│         └── Process: started, ended, participant events        │
└────────────────────────────────────────────────────────────────┘
Complete Implementation
const express = require('express');
const axios = require('axios');
const crypto = require('crypto');

const app = express();
app.use(express.json());

// Track which meetings we care about (use Redis/DB in production)
const trackedMeetings = new Set();

// ============================================
// STEP 1: Retrieve Meeting Details (zoom-rest-api)
// ============================================

async function getAccessToken() {
  const credentials = Buffer.from(
    `${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}`
  ).toString('base64');

  const response = await axios.post(
    `https://zoom.us/oauth/token?grant_type=account_credentials&account_id=${process.env.ZOOM_ACCOUNT_ID}`,
    null,
    { headers: { 'Authorization': `Basic ${credentials}` } }
  );

  return response.data.access_token;
}

async function getMeetingDetails(meetingId) {
  const token = await getAccessToken();

  const response = await axios.get(
    `https://api.zoom.us/v2/meetings/${meetingId}`,
    { headers: { 'Authorization': `Bearer ${token}` } }
  );

  return response.data;
}

// API: Fetch meeting and start tracking it
app.get('/api/meetings/:meetingId', async (req, res) => {
  try {
    const { meetingId } = req.params;

    // Get meeting details
    const meeting = await getMeetingDetails(meetingId);

    // Add to tracked set (events for this meeting will be processed)
    trackedMeetings.add(String(meeting.id));

    console.log(`✅ Now tracking meeting: ${meeting.topic} (${meeting.id})`);

    res.json({
      success: true,
      meeting: {
        id: meeting.id,
        topic: meeting.topic,
        start_time: meeting.start_time,
        join_url: meeting.join_url
      },
      message: 'Meeting tracked. Webhook events will be processed for this meeting.'
    });
  } catch (error) {
    res.status(error.response?.status || 500).json({
      success: false,
      error: error.message
    });
  }
});

// ============================================
// STEP 2: Handle Webhook Events (webhooks)
// ============================================

function verifyWebhookSignature(req) {
  const signature = req.headers['x-zm-signature'];
  const timestamp = req.headers['x-zm-request-timestamp'];
  const payload = `v0:${timestamp}:${JSON.stringify(req.body)}`;

  const expectedSignature = `v0=${crypto
    .createHmac('sha256', process.env.ZOOM_WEBHOOK_SECRET)
    .update(payload)
    .digest('hex')}`;

  return signature === expectedSignature;
}

app.post('/webhooks/zoom', async (req, res) => {
  // Handle URL validation challenge
  if (req.body.event === 'endpoint.url_validation') {
    const hash = crypto
      .createHmac('sha256', process.env.ZOOM_WEBHOOK_SECRET)
      .update(req.body.payload.plainToken)
      .digest('hex');

    return res.json({
      plainToken: req.body.payload.plainToken,
      encryptedToken: hash
    });
  }

  // Verify signature
  if (!verifyWebhookSignature(req)) {
    console.error('❌ Invalid webhook signature');
    return res.status(401).send('Invalid signature');
  }

  const { event, payload } = req.body;
  const meetingId = String(payload.object.id);

  // CRITICAL: Filter events - only process tracked meetings
  if (!trackedMeetings.has(meetingId)) {
    console.log(`⏭️  Ignoring event for untracked meeting ${meetingId}`);
    return res.status(200).send();
  }

  // Process events for tracked meetings
  console.log(`📩 Event: ${event} for meeting ${meetingId}`);

  switch (event) {
    case 'meeting.started':
      console.log(`✅ Meeting started: ${payload.object.topic}`);
      // Handle meeting start
      break;

    case 'meeting.ended':
      console.log(`🏁 Meeting ended: ${payload.object.topic}`);
      // Handle meeting end, cleanup
      trackedMeetings.delete(meetingId);
      break;

    case 'meeting.participant_joined':
      console.log(`👋 ${payload.object.participant.user_name} joined`);
      // Track attendance
      break;

    case 'meeting.participant_left':
      console.log(`👋 ${payload.object.participant.user_name} left`);
      // Update attendance
      break;
  }

  res.status(200).send();
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
  console.log('');
  console.log('Endpoints:');
  console.log('  GET  /api/meetings/:id  - Fetch & track meeting');
  console.log('  POST /webhooks/zoom     - Webhook receiver');
});

See also: meeting-details-with-events.md for expanded webhook implementation.


Pattern 2: REST API + WebSockets (Server-Side Low-Latency Events)

When to use:

  • Lower latency than webhooks required

  • Security-sensitive industries (no exposed endpoint)

  • Bidirectional communication needed

  • Real-time dashboards, live monitoring

Skills needed: zoom-rest-api → zoom-websockets

Flow Diagram
┌────────────────────────────────────────────────────────────────┐
│ SETUP PHASE (One-time in Marketplace Portal):                  │
│                                                                 │
│ 1. Create Server-to-Server OAuth app                           │
│ 2. Configure WebSocket Event Subscription                      │
│    └── Subscribe to: meeting.started, meeting.ended, etc.      │
│    └── Get subscription ID                                     │
└────────────────────────────────────────────────────────────────┘
                             │
                             ▼
┌────────────────────────────────────────────────────────────────┐
│ RUNTIME PHASE:                                                  │
│                                                                 │
│ Step 1: GET /meetings/{meetingId} (zoom-rest-api)              │
│         └── Get meeting details                                │
│                                                                 │
│ Step 2: Connect to WebSocket (zoom-websockets)                 │
│         └── wss://ws.zoom.us/ws?subscriptionId=...             │
│         └── Authenticate with S2S OAuth access token           │
│                                                                 │
│ Step 3: Receive events via WebSocket                           │
│         └── Filter by meeting ID in message handler            │
│         └── Lower latency than webhooks                        │
└────────────────────────────────────────────────────────────────┘
Complete Implementation
const axios = require('axios');
const WebSocket = require('ws');

// Track which meetings we care about
const trackedMeetings = new Set();
let ws = null;

// ============================================
// STEP 1: Retrieve Meeting Details (zoom-rest-api)
// ============================================

async function getAccessToken() {
  const credentials = Buffer.from(
    `${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}`
  ).toString('base64');

  const response = await axios.post(
    'https://zoom.us/oauth/token',
    new URLSearchParams({
      grant_type: 'account_credentials',
      account_id: process.env.ZOOM_ACCOUNT_ID
    }),
    {
      headers: {
        'Authorization': `Basic ${credentials}`,
        'Content-Type': 'application/x-www-form-urlencoded'
      }
    }
  );

  return response.data.access_token;
}

async function getMeetingDetails(meetingId) {
  const token = await getAccessToken();

  const response = await axios.get(
    `https://api.zoom.us/v2/meetings/${meetingId}`,
    { headers: { 'Authorization': `Bearer ${token}` } }
  );

  return response.data;
}

// ============================================
// STEP 2: Connect to WebSocket (zoom-websockets)
// ============================================

async function connectWebSocket() {
  const accessToken = await getAccessToken();
  const subscriptionId = process.env.ZOOM_WEBSOCKET_SUBSCRIPTION_ID;

  const wsUrl = `wss://ws.zoom.us/ws?subscriptionId=${subscriptionId}&access_token=${accessToken}`;

  ws = new WebSocket(wsUrl);

  ws.on('open', () => {
    console.log('✅ WebSocket connection established');
  });

  ws.on('message', (data) => {
    const event = JSON.parse(data);
    handleWebSocketEvent(event);
  });

  ws.on('close', () => {
    console.log('❌ WebSocket connection closed. Reconnecting...');
    setTimeout(connectWebSocket, 5000);
  });

  ws.on('error', (error) => {
    console.error('WebSocket error:', error);
  });
}

// ============================================
// STEP 3: Handle WebSocket Events
// ============================================

function handleWebSocketEvent(event) {
  const { event: eventType, payload } = event;
  const meetingId = String(payload?.object?.id);

  // CRITICAL: Filter events - only process tracked meetings
  if (!meetingId || !trackedMeetings.has(meetingId)) {
    return;
  }

  console.log(`📩 WebSocket Event: ${eventType} for meeting ${meetingId}`);

  switch (eventType) {
    case 'meeting.started':
      console.log(`✅ Meeting started: ${payload.object.topic}`);
      break;

    case 'meeting.ended':
      console.log(`🏁 Meeting ended: ${payload.object.topic}`);
      trackedMeetings.delete(meetingId);
      break;

    case 'meeting.participant_joined':
      console.log(`👋 ${payload.object.participant.user_name} joined`);
      break;

    case 'meeting.participant_left':
      console.log(`👋 ${payload.object.participant.user_name} left`);
      break;
  }
}

// ============================================
// USAGE: Track Meeting and Receive Events
// ============================================

async function trackMeeting(meetingId) {
  // Get meeting details
  const meeting = await getMeetingDetails(meetingId);

  // Add to tracked set
  trackedMeetings.add(String(meeting.id));

  console.log(`✅ Now tracking meeting: ${meeting.topic} (${meeting.id})`);
  console.log(`   Events will arrive via WebSocket connection`);

  return meeting;
}

// Initialize WebSocket connection
connectWebSocket();

// Track specific meetings
trackMeeting('123456789').catch(console.error);
trackMeeting('987654321').catch(console.error);

Comparison with Webhooks:

AspectWebSocketsWebhooks
LatencyLower (persistent connection)Higher (new HTTP request per event)
SetupMore complex (S2S OAuth required)Simpler (just endpoint URL)
SecurityNo exposed endpointRequires public endpoint + signature verification
Best forReal-time dashboards, high-frequency eventsStandard backend processing

Pattern 3: REST API + Meeting SDK Events (Client-Side In-Meeting)

When to use:

  • Building custom meeting UI with real-time updates

  • In-meeting participant tracking

  • User joins meeting from your web app

  • Real-time UI state updates (active speaker, video on/off, etc.)

Skills needed: zoom-rest-api → zoom-meeting-sdk

Flow Diagram
┌────────────────────────────────────────────────────────────────┐
│ RUNTIME PHASE (User-initiated):                                │
│                                                                 │
│ Step 1: GET /meetings/{meetingId} (zoom-rest-api)              │
│         └── Get meeting number, password                       │
│         └── Generate SDK signature (JWT)                       │
│                                                                 │
│ Step 2: ZoomMtg.init() + join() (zoom-meeting-sdk)             │
│         └── User joins meeting in browser                      │
│         └── Receive ZoomMtg instance for event listeners       │
│                                                                 │
│ Step 3: ZoomMtg.inMeetingServiceListener() (zoom-meeting-sdk)  │
│         └── Listen to: onUserJoin, onUserLeave, etc.           │
│         └── Events ONLY for this specific meeting              │
│         └── Real-time UI updates                               │
└────────────────────────────────────────────────────────────────┘
Complete Implementation

Backend: Generate Signature

const KJUR = require('jsrsasign');

// Step 1: Get meeting details
async function getMeetingDetails(meetingId) {
  const token = await getAccessToken();

  const response = await axios.get(
    `https://api.zoom.us/v2/meetings/${meetingId}`,
    { headers: { 'Authorization': `Bearer ${token}` } }
  );

  return {
    id: response.data.id,
    password: response.data.password,
    topic: response.data.topic,
    join_url: response.data.join_url
  };
}

// Generate Meeting SDK signature
function generateSignature(meetingNumber, role) {
  const iat = Math.floor(Date.now() / 1000) - 30;
  const exp = iat + 60 * 60 * 2; // 2 hours

  const header = { alg: 'HS256', typ: 'JWT' };
  const payload = {
    sdkKey: process.env.ZOOM_SDK_KEY,
    mn: String(meetingNumber).replace(/\D/g, ''),
    role: parseInt(role, 10), // 0 = participant, 1 = host
    iat,
    exp,
    tokenExp: exp
  };

  return KJUR.jws.JWS.sign(
    'HS256',
    JSON.stringify(header),
    JSON.stringify(payload),
    process.env.ZOOM_SDK_SECRET
  );
}

// API endpoint: Get meeting details + signature
app.get('/api/meetings/:meetingId/join', async (req, res) => {
  try {
    const { meetingId } = req.params;
    const { role = 0 } = req.query;

    // Get meeting details from Zoom API
    const meeting = await getMeetingDetails(meetingId);

    // Generate signature for SDK
    const signature = generateSignature(meeting.id, role);

    res.json({
      meetingNumber: meeting.id,
      password: meeting.password,
      signature: signature,
      sdkKey: process.env.ZOOM_SDK_KEY,
      topic: meeting.topic
    });
  } catch (error) {
    res.status(error.response?.status || 500).json({
      error: error.message
    });
  }
});

Frontend: Join Meeting and Listen to Events

import { ZoomMtg } from '@zoom/meetingsdk';

// Step 1: Get meeting details + signature from backend
async function getMeetingCredentials(meetingId) {
  const response = await fetch(`/api/meetings/${meetingId}/join`);
  return response.json();
}

// Step 2: Initialize Meeting SDK
async function joinMeeting(meetingId, userName) {
  try {
    // Get meeting details from backend (includes REST API call)
    const credentials = await getMeetingCredentials(meetingId);

    // Preload SDK
    ZoomMtg.preLoadWasm();
    ZoomMtg.prepareWebSDK();

    // Initialize SDK
    ZoomMtg.init({
      leaveUrl: window.location.origin,
      patchJsMedia: true,
      leaveOnPageUnload: true,
      success: () => {
        // Join meeting
        ZoomMtg.join({
          signature: credentials.signature,
          sdkKey: credentials.sdkKey,
          meetingNumber: credentials.meetingNumber,
          passWord: credentials.password,
          userName: userName,
          success: () => {
            console.log('✅ Joined meeting:', credentials.topic);

            // Step 3: Subscribe to in-meeting events
            subscribeToMeetingEvents();
          },
          error: (err) => {
            console.error('Join error:', err);
          }
        });
      },
      error: (err) => {
        console.error('Init error:', err);
      }
    });
  } catch (error) {
    console.error('Failed to join meeting:', error);
  }
}

// Step 3: Listen to Meeting SDK events
function subscribeToMeetingEvents() {
  // User join events
  ZoomMtg.inMeetingServiceListener('onUserJoin', (data) => {
    console.log('👋 User joined:', data);
    // data.userId, data.userName
    // Update UI: add participant to list
  });

  // User leave events
  ZoomMtg.inMeetingServiceListener('onUserLeave', (data) => {
    console.log('👋 User left:', data);
    // Update UI: remove participant from list
  });

  // Active speaker detection
  ZoomMtg.inMeetingServiceListener('onActiveSpeaker', (data) => {
    console.log('🎤 Active speaker:', data);
    // data: [{ userId, userName }]
    // Update UI: highlight speaker
  });

  // Meeting status changes
  ZoomMtg.inMeetingServiceListener('onMeetingStatus', (data) => {
    console.log('📊 Meeting status:', data);
    // status: 1=connecting, 2=connected, 3=disconnected, 4=reconnecting
  });

  // Chat messages
  ZoomMtg.inMeetingServiceListener('onReceiveChatMsg', (data) => {
    console.log('💬 Chat message:', data);
    // Display in custom UI
  });

  // Recording status
  ZoomMtg.inMeetingServiceListener('onRecordingChange', (data) => {
    console.log('🔴 Recording status:', data);
    // Show recording indicator
  });

  // User updates (audio/video state changes)
  ZoomMtg.inMeetingServiceListener('onUserUpdate', (data) => {
    console.log('🔄 User updated:', data);
    // Update UI: show muted/video off indicators
  });
}

// Usage
joinMeeting('123456789', 'John Doe');

Key Differences from Webhooks/WebSockets:

AspectMeeting SDK EventsWebhooks/WebSockets
ScopeMeeting-specific (only joined meeting)Account-level (all meetings)
LocationClient-side (browser)Server-side
AuthenticationSDK signature (JWT)OAuth access token
Use caseIn-meeting UI updatesBackend processing
Event typesParticipant, audio/video, chat, recordingMeeting lifecycle, participants

Pattern 4: REST API + RTMS (Bot-Based Real-Time Media + Events)

When to use:

  • AI transcription, translation, sentiment analysis

  • Meeting recording bots

  • Real-time media processing (audio/video streams)

  • Compliance monitoring

Skills needed: zoom-rest-api → rtms

Flow Diagram
┌────────────────────────────────────────────────────────────────┐
│ SETUP PHASE:                                                    │
│                                                                 │
│ 1. Configure webhook subscription for meeting.rtms_started     │
│ 2. Deploy bot infrastructure                                   │
└────────────────────────────────────────────────────────────────┘
                             │
                             ▼
┌────────────────────────────────────────────────────────────────┐
│ RUNTIME PHASE:                                                  │
│                                                                 │
│ Step 1: GET /meetings/{meetingId} (zoom-rest-api)              │
│         └── Get meeting details                                │
│                                                                 │
│ Step 2: Wait for meeting.rtms_started webhook                  │
│         └── Contains rtmsSessionID, meetingUUID                │
│                                                                 │
│ Step 3: Connect to RTMS WebSocket (rtms)                  │
│         └── wss://rtms.zoom.us/ws                              │
│         └── Subscribe to: audio, video, transcript, chat       │
│                                                                 │
│ Step 4: Receive real-time media + events                       │
│         └── Audio chunks, video frames, transcripts            │
│         └── Events: participant joined/left, speaking status   │
└────────────────────────────────────────────────────────────────┘
Complete Implementation

See comprehensive implementation: rtms/examples/rtms-bot.md

Quick Overview:

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

// Step 1: Get meeting details
async function getMeetingDetails(meetingId) {
  const token = await getAccessToken();

  const response = await axios.get(
    `https://api.zoom.us/v2/meetings/${meetingId}`,
    { headers: { 'Authorization': `Bearer ${token}` } }
  );

  return response.data;
}

// Step 2: Wait for meeting.rtms_started webhook
app.post('/webhooks/zoom', (req, res) => {
  const { event, payload } = req.body;

  if (event === 'meeting.rtms_started') {
    const { rtmsSessionID, meetingUUID } = payload.object;

    // Step 3: Connect to RTMS
    connectToRTMS(rtmsSessionID, meetingUUID);
  }

  res.status(200).send();
});

// Step 3: Connect to RTMS WebSocket
async function connectToRTMS(sessionID, meetingUUID) {
  const token = await getAccessToken();

  const ws = new WebSocket('wss://rtms.zoom.us/ws', {
    headers: {
      'Authorization': `Bearer ${token}`,
      'X-Zoom-Session-ID': sessionID
    }
  });

  ws.on('open', () => {
    // Subscribe to media types
    ws.send(JSON.stringify({
      type: 'subscribe',
      mediaTypes: ['audio', 'transcript', 'chat']
    }));
  });

  ws.on('message', (data) => {
    const message = JSON.parse(data);

    // Step 4: Handle real-time media and events
    switch (message.type) {
      case 'audio':
        processAudioChunk(message.data);
        break;

      case 'transcript':
        console.log(`📝 Transcript: ${message.data.text}`);
        break;

      case 'chat':
        console.log(`💬 Chat: ${message.data.message}`);
        break;

      case 'participant_joined':
        console.log(`👋 ${message.data.user_name} joined`);
        break;

      case 'participant_left':
        console.log(`👋 ${message.data.user_name} left`);
        break;
    }
  });
}

function processAudioChunk(audioData) {
  // Process raw audio for transcription, analysis, etc.
}

RTMS Events (Different from Webhooks):

Event TypeSourceData
participant_joinedRTMS WebSocketReal-time when user joins
participant_leftRTMS WebSocketReal-time when user leaves
active_speaker_changeRTMS WebSocketCurrent speaker info
audioRTMS WebSocketRaw audio stream (PCM)
videoRTMS WebSocketRaw video frames
transcriptRTMS WebSocketLive transcription

Pattern 5: REST API + Reports (Historical Data, Not Real Events)

When to use:

  • Historical analysis, batch processing

  • Audit logs, compliance reports

  • No real-time requirements

  • Backup/recovery systems

Skills needed: zoom-rest-api only

Implementation
// Get meeting details
async function getMeetingDetails(meetingId) {
  const token = await getAccessToken();

  const response = await axios.get(
    `https://api.zoom.us/v2/meetings/${meetingId}`,
    { headers: { 'Authorization': `Bearer ${token}` } }
  );

  return response.data;
}

// Poll for meeting participants report (after meeting ends)
async function getMeetingParticipantsReport(meetingId) {
  const token = await getAccessToken();

  // Get past meeting details
  const response = await axios.get(
    `https://api.zoom.us/v2/past_meetings/${meetingId}`,
    { headers: { 'Authorization': `Bearer ${token}` } }
  );

  return response.data;
}

// Get detailed participant report
async function getParticipantsList(meetingId) {
  const token = await getAccessToken();

  const response = await axios.get(
    `https://api.zoom.us/v2/report/meetings/${meetingId}/participants`,
    { headers: { 'Authorization': `Bearer ${token}` } }
  );

  return response.data.participants;
}

// Usage: Retrieve meeting + get historical data
async function analyzeMeeting(meetingId) {
  // Get meeting metadata
  const meeting = await getMeetingDetails(meetingId);
  console.log(`Meeting: ${meeting.topic}`);

  // Wait for meeting to end, then get reports
  // (typically poll or wait for meeting.ended webhook)

  const participants = await getParticipantsList(meetingId);
  console.log(`Total participants: ${participants.length}`);

  participants.forEach(p => {
    console.log(`- ${p.name}: ${p.duration} minutes`);
  });
}

Not Real-Time Events:

  • Reports API only provides historical data after meeting ends

  • Not suitable for real-time monitoring

  • No WebSocket/webhook integration

  • Use for: auditing, analytics, compliance


Comparison: When to Use Each Pattern

PatternLatencyScopeSetup ComplexityBest For
WebhooksMediumAccount-levelLowStandard backend processing
WebSocketsLowAccount-levelMediumReal-time dashboards, no exposed endpoint
Meeting SDKVery LowMeeting-specificLowIn-meeting UI, participant views
RTMSVery LowMeeting-specificHighAI transcription, recording bots
Reports APIN/A (historical)Account-levelLowBatch processing, analytics

Skill Chaining Summary

ScenarioSkills ChainOrder
Backend event processingzoom-rest-api → webhooks1. Get meeting, 2. Filter webhook events
Low-latency monitoringzoom-rest-api → zoom-websockets1. Get meeting, 2. Filter WebSocket events
Custom meeting UIzoom-rest-api → zoom-meeting-sdk1. Get details, 2. Join meeting, 3. Listen to SDK events
AI transcription botzoom-rest-api → rtms1. Get meeting, 2. Wait for webhook, 3. Connect to RTMS
Historical analysiszoom-rest-api only1. Get meeting, 2. Poll reports after meeting ends

Authorization Requirements

All patterns require different OAuth scopes:

PatternRequired Scopes
REST APImeeting:read:admin or meeting:read
WebhooksEvent subscription in app config
WebSocketsServer-to-Server OAuth + event subscription
Meeting SDKSDK Key/Secret + signature generation
RTMSmeeting:read:admin + RTMS-enabled account

See also: Authorization Patterns for complete scope validation strategies.


Common Pitfalls

❌ Misconception: "Subscribe to Events for a Specific Meeting"

Reality: Most Zoom event systems are account-level, not meeting-specific.

What You ThinkWhat Actually Happens
"Subscribe to events for meeting 123456789"You receive events for ALL meetings in the account
"Dynamically enable webhooks when meeting starts"Webhooks are configured at app creation time
"Create a webhook subscription per meeting"One subscription receives events for all meetings

Solution: Filter events by meeting ID in your handler code.

❌ Misconception: "All Events Are the Same"

Reality: Different event systems provide different event types:

Event SystemEvents
WebhooksMeeting lifecycle: started, ended, participant_joined/left, recording_completed
WebSocketsSame as webhooks, but lower latency
Meeting SDKIn-meeting: onUserJoin, onActiveSpeaker, onUserUpdate, onReceiveChatMsg
RTMSReal-time media: participant events + audio/video streams + transcripts

Solution: Choose the right event system for your use case (see comparison table above).



Resources

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

What our scanner noticed· Medium risk

  • Possible data-exfiltration endpoints
  • Instructions that use the network

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/general · 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 zoom-general.zip