@zoom/rtms
    Preparing search index...

    @zoom/rtms

    Zoom Realtime Media Streams (RTMS) SDK

    Bindings for real-time audio, video, and transcript streams from Zoom Meetings

    npm PyPI docs

    The RTMS SDK works with multiple Zoom products:

    See examples/ for complete guides and code samples.

    Language Status Supported Platforms
    Node.js ✅ Supported darwin-arm64, linux-x64
    Python ✅ Supported darwin-arm64, linux-x64
    Go 📅 Planned -

    We are actively working to expand both language and platform support in future releases.

    The RTMS SDK allows developers to:

    • Connect to live Zoom meetings
    • Process real-time media streams (audio, video, transcripts)
    • Receive events about session and participant updates
    • Build applications that interact with Zoom meetings in real-time
    • Handle webhook events with full control over validation and responses

    ⚠️ Requirements: Node.js >= 20.3.0 (Node.js 24 LTS recommended)

    The RTMS SDK uses N-API versions 9 and 10, which require Node.js 20.3.0 or higher.

    # Check your Node.js version
    node --version

    # Install the package
    npm install @zoom/rtms

    If you're using an older version of Node.js:

    # Using nvm (recommended)
    nvm install 24 # Install Node.js 24 LTS (recommended)
    nvm use 24

    # Or install Node.js 20 LTS (minimum)
    nvm install 20
    nvm use 20

    # Reinstall the package
    npm install @zoom/rtms

    Download Node.js: https://nodejs.org/

    The Node.js package provides both class-based and singleton APIs for connecting to RTMS streams.

    ⚠️ Requirements: Python >= 3.10 (Python 3.10, 3.11, 3.12, or 3.13)

    The RTMS SDK requires Python 3.10 or higher.

    # Check your Python version
    python3 --version

    # Install from PyPI
    pip install rtms

    If you're using an older version of Python:

    # Using pyenv (recommended)
    pyenv install 3.12
    pyenv local 3.12

    # Or using your system's package manager
    # Ubuntu/Debian: sudo apt install python3.12
    # macOS: brew install python@3.12

    Download Python: https://www.python.org/downloads/

    The Python package provides a Pythonic decorator-based API with full feature parity to Node.js.

    This project uses Task (go-task) for development builds and testing.

    If you're an end user installing via npm or pip, you don't need Task - the installation will work automatically using prebuilt binaries.

    If you're a contributor building from source, you'll need to install Task:

    macOS:

    brew install go-task
    

    Linux:

    sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b ~/.local/bin
    

    Quick Start for Contributors:

    # Verify your environment meets requirements
    task doctor

    # Setup the project (fetch SDK, install dependencies)
    task setup

    # Build for your platform
    task build:js # Build Node.js bindings
    task build:py # Build Python bindings

    # Run tests
    task test:js # Test Node.js
    task test:py # Test Python

    # See all available commands
    task --list

    For detailed contribution guidelines, build instructions, and troubleshooting, see CONTRIBUTING.md.

    Speaker Identification with Mixed Audio

    When using AUDIO_MIXED_STREAM (the default), all participants are mixed into a single audio stream and the audio callback's metadata will not identify the current speaker. To identify who is speaking, register an onActiveSpeakerEvent callback — it fires whenever the active speaker changes. See Troubleshooting #7 and #80 for details.

    Easily respond to Zoom webhooks and connect to RTMS streams:

    import rtms from "@zoom/rtms";

    // CommonJS
    // const rtms = require('@zoom/rtms').default;

    rtms.onWebhookEvent(({event, payload}) => {
    if (event !== "meeting.rtms_started") return;

    const client = new rtms.Client();

    client.onAudioData((data, timestamp, metadata) => {
    console.log(`Received audio: ${data.length} bytes from ${metadata.userName}`);
    });

    client.join(payload);
    });

    For advanced use cases requiring custom webhook validation or response handling (e.g., Zoom's webhook validation challenge), you can use the enhanced callback with raw HTTP access:

    import rtms from "@zoom/rtms";

    rtms.onWebhookEvent((payload, req, res) => {
    // Access request headers for webhook validation
    const signature = req.headers['x-zoom-signature'];

    // Handle Zoom's webhook validation challenge
    if (req.headers['x-zoom-webhook-validator']) {
    const validationToken = req.headers['x-zoom-webhook-validator'];

    // Echo back the validation token
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ plainToken: validationToken }));
    return;
    }

    // Custom validation logic
    if (!validateWebhookSignature(payload, signature)) {
    res.writeHead(401, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ error: 'Invalid signature' }));
    return;
    }

    // Process the webhook payload
    if (payload.event === "meeting.rtms_started") {
    const client = new rtms.Client();

    client.onAudioData((data, timestamp, metadata) => {
    console.log(`Received audio from ${metadata.userName}`);
    });

    client.join(payload.payload);
    }

    // Send custom response
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ status: 'ok' }));
    });

    If you need to integrate webhook handling with your existing Express, Fastify, or other HTTP server (useful for Cloud Run, Kubernetes, or any deployment requiring a single port), use createWebhookHandler:

    import express from 'express';
    import rtms from '@zoom/rtms';

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

    // Your existing application routes
    app.get('/health', (req, res) => {
    res.json({ status: 'healthy' });
    });

    app.get('/admin', (req, res) => {
    res.json({ admin: 'panel' });
    });

    // Create a webhook handler that can be mounted on your existing server
    const webhookHandler = rtms.createWebhookHandler(
    (payload) => {
    console.log(`Received webhook: ${payload.event}`);

    if (payload.event === "meeting.rtms_started") {
    const client = new rtms.Client();
    client.onAudioData((data, timestamp, metadata) => {
    console.log(`Audio from ${metadata.userName}`);
    });
    client.join(payload.payload);
    }
    },
    '/zoom/webhook' // Path to handle
    );

    // Mount the webhook handler on your Express app
    app.post('/zoom/webhook', webhookHandler);

    // Single port for all routes
    const PORT = process.env.PORT || 8080;
    app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
    console.log(`Webhook endpoint: http://localhost:${PORT}/zoom/webhook`);
    console.log(`Health check: http://localhost:${PORT}/health`);
    });

    You can also use RawWebhookCallback with createWebhookHandler for custom validation:

    const webhookHandler = rtms.createWebhookHandler(
    (payload, req, res) => {
    // Custom validation with raw HTTP access
    const signature = req.headers['x-zoom-signature'];

    if (!validateSignature(payload, signature)) {
    res.writeHead(401);
    res.end('Unauthorized');
    return;
    }

    // Process webhook...

    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ status: 'ok' }));
    },
    '/zoom/webhook'
    );

    app.post('/zoom/webhook', webhookHandler);

    For greater control or connecting to multiple streams simultaneously:

    import rtms from "@zoom/rtms";

    const client = new rtms.Client();

    client.onAudioData((data, timestamp, metadata) => {
    console.log(`Received audio: ${data.length} bytes`);
    });

    client.join({
    meeting_uuid: "your_meeting_uuid",
    rtms_stream_id: "your_stream_id",
    server_urls: "wss://example.zoom.us",
    });

    When you only need to connect to a single RTMS stream:

    import rtms from "@zoom/rtms";

    rtms.onAudioData((data, timestamp, metadata) => {
    console.log(`Received audio from ${metadata.userName}`);
    });

    rtms.join({
    meeting_uuid: "your_meeting_uuid",
    rtms_stream_id: "your_stream_id",
    server_urls: "wss://rtms.zoom.us"
    });
    #!/usr/bin/env python3
    import rtms
    import signal
    import sys
    from dotenv import load_dotenv

    load_dotenv()

    client = rtms.Client()

    # Graceful shutdown handler
    def signal_handler(sig, frame):
    print('\nShutting down gracefully...')
    client.leave()
    sys.exit(0)

    signal.signal(signal.SIGINT, signal_handler)

    # Webhook event handler
    @client.on_webhook_event()
    def handle_webhook(payload):
    if payload.get('event') == 'meeting.rtms_started':
    rtms_payload = payload.get('payload', {})
    client.join(
    meeting_uuid=rtms_payload.get('meeting_uuid'),
    rtms_stream_id=rtms_payload.get('rtms_stream_id'),
    server_urls=rtms_payload.get('server_urls'),
    signature=rtms_payload.get('signature')
    )

    # Callback handlers
    @client.onJoinConfirm
    def on_join(reason):
    print(f'Joined meeting: {reason}')

    @client.onTranscriptData
    def on_transcript(data, size, timestamp, metadata):
    text = data.decode('utf-8')
    print(f'[{metadata.userName}]: {text}')

    @client.onLeave
    def on_leave(reason):
    print(f'Left meeting: {reason}')

    if __name__ == '__main__':
    print('Webhook server running on http://localhost:8080')
    import time
    while True:
    # Process queued join requests from webhook thread
    client._process_join_queue()
    # Poll for SDK events
    client._poll_if_needed()
    time.sleep(0.01)

    For production use cases requiring custom webhook validation:

    import rtms
    import hmac
    import hashlib

    client = rtms.Client()

    @client.on_webhook_event()
    def handle_webhook(payload, request, response):
    # Access request headers for validation
    signature = request.headers.get('x-zoom-signature')

    # Handle Zoom's webhook validation challenge
    if request.headers.get('x-zoom-webhook-validator'):
    validator = request.headers['x-zoom-webhook-validator']
    response.set_status(200)
    response.send({'plainToken': validator})
    return

    # Custom signature validation
    if not validate_signature(payload, signature):
    response.set_status(401)
    response.send({'error': 'Invalid signature'})
    return

    # Process valid webhook
    if payload.get('event') == 'meeting.rtms_started':
    client.join(payload.get('payload'))

    response.send({'status': 'ok'})

    Create a virtual environment and install dependencies:

    # Create virtual environment
    python3 -m venv .venv

    # Activate virtual environment
    source .venv/bin/activate # On Windows: .venv\Scripts\activate

    # Install dependencies
    pip install python-dotenv

    # Install RTMS SDK
    pip install rtms

    Create a .env file:

    # Required - Your Zoom OAuth credentials
    ZM_RTMS_CLIENT=your_client_id
    ZM_RTMS_SECRET=your_client_secret

    # Optional - Webhook server configuration
    ZM_RTMS_PORT=8080
    ZM_RTMS_PATH=/webhook

    # Optional - Logging configuration
    ZM_RTMS_LOG_LEVEL=debug # error, warn, info, debug, trace
    ZM_RTMS_LOG_FORMAT=progressive # progressive or json
    ZM_RTMS_LOG_ENABLED=true # true or false

    The RTMS SDK can be built from source using either Docker (recommended) or local build tools.

    • Docker and Docker Compose
    • Zoom RTMS C SDK files (contact Zoom for access)
    • Task installed (or use Docker's Task installation)
    # Clone the repository
    git clone https://github.com/zoom/rtms.git
    cd rtms

    # Place your SDK library files in the lib/{arch} folder
    # For linux-x64:
    cp ../librtmsdk.0.2025xxxx/librtmsdk.so.0 lib/linux-x64

    # For darwin-arm64 (Apple Silicon):
    cp ../librtmsdk.0.2025xxxx/librtmsdk.dylib lib/darwin-arm64

    # Place the include files in the proper directory
    cp ../librtmsdk.0.2025xxxx/h/* lib/include

    # Build using Docker Compose with Task
    docker compose run --rm build task build:js # Build Node.js for linux-x64
    docker compose run --rm build task build:py # Build Python wheel for linux-x64

    # Or use convenience services
    docker compose run --rm test-js # Build and test Node.js
    docker compose run --rm test-py # Build and test Python

    Docker Compose creates distributable packages for linux-x64 (prebuilds for Node.js, wheels for Python). Use this when developing on macOS to build Linux packages for distribution.

    • Node.js (>= 20.3.0, LTS recommended)
    • Python 3.10+ with pip (for Python build)
    • CMake 3.25+
    • C/C++ build tools
    • Task (go-task) - https://taskfile.dev/installation/
    • Zoom RTMS C SDK files (contact Zoom for access)
    # Install system dependencies
    ## macOS
    brew install cmake go-task python@3.13 node@24

    ## Linux
    sudo apt update
    sudo apt install -y cmake python3-full python3-pip npm
    sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b ~/.local/bin

    # Clone and set up the repository
    git clone https://github.com/zoom/rtms.git
    cd rtms

    # Place SDK files in the appropriate lib directory
    # lib/linux-x64/ or lib/darwin-arm64/

    # Verify your environment meets requirements
    task doctor

    # Setup project (fetches SDK if not present, installs dependencies)
    task setup

    # Build for specific language and platform
    task build:js # Build Node.js for current platform
    task build:js:linux # Build Node.js for Linux (via Docker)
    task build:js:darwin # Build Node.js for macOS

    task build:py # Build Python for current platform
    task build:py:linux # Build Python wheel for Linux (via Docker)
    task build:py:darwin # Build Python wheel for macOS

    The project uses Task (go-task) for build orchestration. Commands follow the pattern: task <action>:<lang>:<platform>

    # See all available commands
    task --list

    # Verify environment
    task doctor # Check Node, Python, CMake, Docker versions

    # Setup project
    task setup # Fetch SDK and install dependencies

    # Build modes
    BUILD_TYPE=Debug task build:js # Build in debug mode
    BUILD_TYPE=Release task build:js # Build in release mode (default)

    # Debug logging for C SDK callbacks
    RTMS_DEBUG=ON task build:js # Enable verbose callback logging

    If you encounter issues:

    Symptoms:

    • Immediate crash when requiring/importing the module
    • Error message: Segmentation fault (core dumped)
    • Stack trace shows napi_module_register_by_symbol

    Root Cause: Using Node.js version < 20.3.0

    Solution:

    # 1. Check your Node.js version
    node --version

    # 2. If < 20.3.0, upgrade to a supported version

    # Using nvm (recommended):
    nvm install 24 # Install Node.js 24 LTS (recommended)
    nvm use 24

    # Or install minimum version:
    nvm install 20
    nvm use 20

    # Or download from: https://nodejs.org/

    # 3. Clear npm cache and reinstall
    npm cache clean --force
    rm -rf node_modules package-lock.json
    npm install

    Prevention:

    • Always use Node.js 20.3.0 or higher
    • Use recommended version with .nvmrc: nvm use (Node.js 24 LTS)
    • Check version before installing: node --version

    Verify you're using a supported platform (darwin-arm64 or linux-x64)

    Ensure RTMS C SDK files are correctly placed in the appropriate lib directory

    Try both debug and release modes (npm run debug or npm run release)

    Verify all prerequisites are installed

    This SDK uses different default audio parameters than the raw RTMS WebSocket protocol for better out-of-the-box quality. If you need to match the WebSocket protocol defaults, see #92 for details.

    When using AUDIO_MIXED_STREAM, the audio callback's metadata does not identify the current speaker since all participants are mixed into a single stream. To identify who is speaking, use the onActiveSpeakerEvent callback:

    Node.js:

    client.onActiveSpeakerEvent((timestamp, userId, userName) => {
    console.log(`Active speaker: ${userName} (${userId})`);
    });

    Python:

    @client.onActiveSpeakerEvent
    def on_active_speaker(timestamp, user_id, user_name):
    print(f"Active speaker: {user_name} ({user_id})")

    This callback notifies your application whenever the active speaker changes in the meeting. You can also use the lower-level onEventEx function with the active speaker event type directly. See #80 for more details.

    If you're a maintainer looking to build, test, or publish new releases of the RTMS SDK, please refer to PUBLISHING.md for comprehensive documentation on:

    • Building platform-specific wheels and prebuilds
    • Publishing to npm and PyPI
    • GitHub Actions CI/CD workflow
    • Testing procedures
    • Troubleshooting common issues
    • Release workflows for Node.js and Python

    This project is licensed under the MIT License - see the LICENSE.md file for details.