How to migrate a Zoom meeting bot to RTMS

Replace your Meeting SDK bot with Zoom RTMS for native transcript, audio, and video access. No participant tile, no waiting room friction, same data.

Built with

Anthropic
OpenAI

Updated November 2026

RTMS, Zoom Apps, Meeting SDK

RTMS access via Zoom Developer Pack

Node · Express · React · MySQL

Your meeting bot works. Users see it join. Hosts admit it from the waiting room. You manage infrastructure to keep a participant connected for the duration of the call. Zoom RTMS gives you the same data (audio, video, transcripts, screen share) without the bot.

Meeting SDK bots join as participants. They appear in the roster, require host approval, and need compute resources to stay connected. RTMS streams meeting data directly to your backend via webhook and WebSocket. No participant tile. No waiting room. The user installs your app once, and every meeting streams automatically.

What you'll need:

  • An existing Meeting SDK bot you want to migrate (or understanding of bot architecture)
  • Transcript access via RTMS  (available through the Zoom Developer Pack ; try 20 credits free)
  • A backend to receive webhooks and maintain WebSocket connections
  • A Zoom General App  with RTMS event subscriptions
  • Optional: A Zoom Surface App  for in-meeting UI

What changes:

  • No participant: RTMS streams data without joining the meeting. Users see your app in the Apps panel, not the participant list.
  • Webhook-driven lifecycle: Instead of joining via SDK, you receive meeting.rtms_started when streaming begins.
  • User-scoped authorization: The app is installed per-user (or org-wide by admin). The user's presence determines stream availability.
  • Host approval model: Hosts can require approval before RTMS starts. Configure auto-start to skip this for trusted apps.

What stays the same:

  • Real-time transcript segments with speaker attribution
  • Mixed or separated audio streams (PCM L16, 16kHz or 24kHz)
  • Video frames (H.264, gallery view)
  • Screen share capture
  • Participant join/leave events

If you're building a new meeting integration from scratch, start with the AI Meeting Notetaker blueprint instead. This guide is for developers migrating an existing bot.

Don't want to build? Preferred partners Recall.ai , Attendee , and MeetStream  offer managed RTMS infrastructure. They handle the WebSocket connections and media processing; you receive structured data via their APIs.

Follow along as we walk through the migration path.

Features

This migration guide covers:

  • Side-by-side architecture comparison: Bot vs. RTMS data flow and component differences
  • Decision matrix: When to stay on Meeting SDK vs. migrate to RTMS
  • Webhook handler setup: Replace SDK join with event-driven stream initiation
  • WebSocket connection patterns: Connect to RTMS streams using the @zoom/rtms SDK
  • Lifecycle management: Handle stream start, stop, and reconnection scenarios
  • Stream persistence for long meetings: The scheduler-as-app-owner pattern for virtual onsites
  • Data model migration: Update session tracking from bot IDs to RTMS stream IDs
  • Chat alternatives: Replace bot chat with REST API, Surface Apps, or pre-meeting notifications
  • Failure modes and fallback logic: Detect failures and fall back gracefully
  • Compliance considerations: Recording notification, consent, pause/cancel functionality
  • Enterprise deployment: Admin install flow for org-wide rollout

The reference implementation is Arlo , a meeting assistant that demonstrates the complete RTMS pattern.

Architecture

Meeting SDK bots and RTMS solve the same problem differently. Understanding the architectural shift is the first step.

Bot Architecture (before)

Your bot runs as a headless participant. It joins via Meeting SDK, authenticates with a ZAK or OAuth token, and captures media through SDK callbacks. The bot process must stay connected for the meeting duration.

graph LR
    subgraph "Your Infrastructure"
        B[Bot Process]
        S[Your Server]
    end
    Z[Zoom Meeting] -->|SDK join| B
    B -->|Audio/Video callbacks| B
    B -->|Processed data| S
    S --> D[(Database)]

Characteristics:

  • Bot appears as a participant (visible in roster)
  • Requires compute resources per concurrent meeting
  • Must handle SDK lifecycle (join, reconnect, leave)
  • Host may need to admit bot from waiting room

RTMS Architecture (after)

RTMS streams data directly to your backend. Zoom sends a webhook when streaming starts, your server connects via WebSocket, and media flows without a participant joining.

graph LR
    subgraph "Your Infrastructure"
        W[Webhook Endpoint]
        R[RTMS Client]
        S[Your Server]
    end
    Z[Zoom Meeting] -->|1. meeting.rtms_started| W
    W -->|2. Connect| R
    Z -->|3. WebSocket stream| R
    R -->|4. Processed data| S
    S --> D[(Database)]

Characteristics:

  • No participant tile (app appears in Apps panel only)
  • Webhook-driven: Zoom initiates the connection
  • Single WebSocket per stream (not per participant)
  • User installs app once; all their meetings can stream

Migration Decision Matrix

CapabilityMeeting SDK BotRTMSNotes
Receive audioYesYesRTMS offers mixed or per-participant
Receive videoYesYes (gallery)Speaker view on Zoom roadmap
Receive transcriptManual (ASR)Yes (built-in)RTMS includes Zoom's ASR
Receive screen shareYesYesJPEG/PNG or H.264
Send audioYesNoBots can speak; RTMS is receive-only
Send chatYesNoUse REST API or Surface App
Control meetingYesNoMute, end, etc. require SDK
Visible participantYesNoRTMS has no roster presence
Breakout roomsYesNoRTMS main room only
Per-meeting computeRequiredNot requiredRTMS uses shared infrastructure

When to stay on Meeting SDK:

  • Your bot needs to speak (voice assistant)
  • Your bot sends chat messages during the meeting
  • Your bot controls meeting state (mute participants, end meeting)
  • You need breakout room access

When to migrate to RTMS:

  • Receive-only workloads (transcription, recording, analysis)
  • Users complain about "bot in my meeting"
  • You want to reduce infrastructure costs
  • You need Zoom's built-in ASR instead of running your own

Implementation Guide

This guide covers the key migration steps. For complete code, see the Arlo reference implementation .

Part 1: Replace SDK Join with Webhook Handler

Bot approach (before)

Your bot joins meetings by calling the Meeting SDK with credentials:

// Meeting SDK bot approach
const client = new ZoomMeetingSDK();
await client.join({
  meetingNumber: meetingId,
  userName: 'Recording Bot',
  zak: zakToken,
  password: meetingPassword
});

client.on('audio', (buffer) => { /* process audio */ });
client.on('video', (frame) => { /* process video */ });

RTMS approach (after)

With RTMS, Zoom calls you. Set up a webhook endpoint to receive meeting.rtms_started:

Input: meeting.rtms_started webhook payload

Output: Validated webhook, RTMS session initiated

Invariants:

  • Verify webhook signature using HMAC-SHA256 with timing-safe comparison
  • Enforce 5-minute replay window on timestamp
  • Respond with 200 OK within 3 seconds (process asynchronously)
  • Deduplicate by rtms_stream_id (not meeting_uuid)
// Webhook signature verification (security-critical, keep concrete)
function verifyWebhookSignature(rawBody, timestamp, signature, secret) {
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp, 10)) > 300) return false;

  const message = `v0:${timestamp}:${rawBody.toString('utf8')}`;
  const expected = 'v0=' + crypto
    .createHmac('sha256', secret)
    .update(message)
    .digest('hex');

  // timingSafeEqual throws if the buffers differ in length, so check first.
  const received = Buffer.from(String(signature));
  const computed = Buffer.from(expected);
  if (received.length !== computed.length) return false;
  return crypto.timingSafeEqual(received, computed);
}

Other languages: In Python, use hmac.compare_digest; in Go, use crypto/subtle.ConstantTimeCompare.

See Arlo's webhook handler  for the complete implementation.

Part 2: Connect to RTMS WebSocket

After validating the webhook, connect to the RTMS stream using the provided server_urls.

Input: server_urls, rtms_stream_id, meeting_uuid from webhook payload

Output: Active WebSocket connection receiving media frames

Invariants:

  • Use the @zoom/rtms SDK for Node.js (or implement WebSocket protocol directly for other languages)
  • Register all media handlers before calling join()
  • Handle onLeave callback for cleanup
  • Track connection state per rtms_stream_id
  • If stream drops mid-meeting, wait for new meeting.rtms_started webhook
// RTMS connection pattern
const { RTMSClient } = require('@zoom/rtms');

async function connectToStream(payload) {
  const client = new RTMSClient({
    streamId: payload.rtms_stream_id,
    serverUrls: payload.server_urls,
    credentials: { /* from your app config */ }
  });

  // Register handlers BEFORE joining
  client.on('transcript', (segment) => {
    // segment: { text, userId, userName, timestamp }
    processTranscript(segment);
  });

  client.on('audio', (buffer) => {
    // PCM L16 audio data
    processAudio(buffer);
  });

  client.on('leave', () => {
    cleanupSession(payload.rtms_stream_id);
  });

  await client.join();
}

Other languages: The @zoom/rtms SDK is Node.js only. For Python, Go, or other stacks, implement the WebSocket protocol  directly.

Part 3: Handle Lifecycle Differences

Meeting SDK bots control their own lifecycle. RTMS streams are controlled by users and hosts. This is the most significant behavioral change in the migration.

Stream start conditions

RTMS starts when ALL conditions are met:

  • The meeting host's organization has RTMS enabled at the account level
  • The meeting host's Zoom client is version 6.5.5+
  • The app is installed for the user (app owner)
  • Account/group/user policy allows RTMS
  • Host approves (if required) or auto-start is enabled
  • At least one app owner is in the meeting (or auto-start triggered before join)

Stream persistence

EventBot BehaviorRTMS Behavior
Host leavesBot staysStream continues IF app owner used auto-start and never joined. Stream STOPS if app owner joined the meeting.
App owner leavesN/AStream stops
Host disables sharingBot unaffectedStream stops; new rtms_started if re-enabled
Host handoff to org with RTMS disabledBot unaffectedStream stops permanently
App owner moves to breakout roomN/AStream stops; may resume when returning to main room
Network disconnectBot reconnectsWebSocket closes; must wait for new meeting.rtms_started webhook

WARNING: App owner leaving kills the stream. This is the most common migration surprise. With bots, you control when to leave. With RTMS, if the app owner leaves the meeting, the stream terminates even if other participants remain. Plan for this in your product flow.

Stream persistence for long meetings (virtual onsites)

For multi-hour meetings where participants rotate (virtual onsites, all-day workshops, interview panels), you need the stream to persist regardless of who joins or leaves. The key insight:

If the app owner is the meeting's original scheduler AND auto-start is enabled, the RTMS stream persists for the entire meeting lifetime. The scheduler does not need to join or stay in the meeting.

This means:

  • Your app must be installed for the meeting scheduler (the person who created the meeting), not just the participants
  • Use the scheduler's OAuth credentials when configuring auto-start for the meeting
  • If your calendar sync knows who scheduled the meeting, use that user's context

When the scheduler is not your user: If an external party scheduled the meeting (candidate, external recruiter, customer), you cannot use their credentials for auto-start. Options:

  1. Fall back to a participant who is your user, accepting the risk that the stream may die if they leave
  2. Prompt the external scheduler to install your app (may not be practical)
  3. For critical meetings, keep a bot fallback ready

Handling multiple users

When multiple users in the same meeting have your app installed, Zoom may send multiple meeting.rtms_started webhooks (one per app owner). Deduplicate by meeting_uuid:

Invariants:

  • Process only the first webhook per meeting_uuid
  • Ignore subsequent webhooks while a stream is active
  • If active stream drops and new webhook arrives, treat as reconnection
  • Track which user's stream you're using; if that user leaves and a new webhook arrives from another user, you can switch

Part 4: Update Your Data Model

Your existing bot likely tracks sessions by meeting ID and bot instance. RTMS uses different identifiers.

Bot ModelRTMS Model
bot_id (your instance)rtms_stream_id (Zoom-assigned)
meeting_id (numeric)meeting_uuid (string, unique per instance)
participant_id (bot's ID)N/A (no participant)
Bot status (joining, connected, leaving)Stream status (waiting, connected, ended)

Schema migration:

-- Add RTMS columns to existing sessions table
ALTER TABLE meeting_sessions ADD COLUMN rtms_stream_id VARCHAR(255);
ALTER TABLE meeting_sessions ADD COLUMN stream_status ENUM('waiting', 'connected', 'ended');

-- Index for webhook deduplication
CREATE UNIQUE INDEX idx_rtms_stream ON meeting_sessions(rtms_stream_id);

Part 5: Replace Chat with Alternatives

Meeting SDK bots can send chat messages. RTMS cannot. If your bot sends messages (recording consent, cancel links), you have three options:

Option A: REST API (if continuous chat enabled)

If the meeting has continuous chat enabled, use the Chat Messages API :

await zoom.chat.sendMessage({
  to_channel: meetingChatChannel,
  message: 'Recording in progress. Reply STOP to cancel.'
});

Option B: Surface App

Build a Zoom Surface App  that displays recording status and cancel button in-meeting. The Surface App can use the Zoom Apps SDK sendMessage() method to send chat.

Option C: Pre-meeting notification

Send recording consent via email or calendar invite before the meeting starts. Less interactive but simpler.

Part 6: Plan for Failure Modes

Bots fail visibly: the bot doesn't join, or it disconnects and participants notice. RTMS fails silently: no webhook arrives, or the stream drops, and participants have no idea. Build detection and fallback logic.

Failure modes table

Failure ModeHow to DetectMitigation
RTMS not enabled for host orgNo meeting.rtms_started webhook within 60 seconds of meeting startFall back to bot; notify your ops team
Zoom client too old (< 6.5.5)No meeting.rtms_started receivedFall back to bot; prompt user to update Zoom
Host denies RTMS approvalNo meeting.rtms_started, or meeting.rtms_stopped fires immediatelyFall back to bot; notify user that host declined
WebSocket connection failureConnection error or timeout when connecting to server_urlsRetry with exponential backoff; fall back to bot if retries exhausted
Stream drops mid-meetingWebSocket close event without meeting.rtms_stoppedLog the gap timestamp; wait 30 seconds for new meeting.rtms_started; if none arrives, finalize partial recording
App owner leaves (stream dies)meeting.rtms_stopped webhook while meeting is still activeIf another app owner is in the meeting, their webhook may arrive; otherwise finalize partial recording
Duplicate webhooksMultiple meeting.rtms_started for same meeting_uuidDeduplicate; process only one stream; ignore others
Webhook signature invalidHMAC verification failsReject the request; log for security review
Webhook replay attackTimestamp outside 5-minute windowReject the request

Fallback decision logic

Meeting scheduled
  │
  ├── Is scheduler an app owner with RTMS installed?
  │     ├── YES → Configure auto-start with scheduler's credentials
  │     └── NO  → Use first participant who is an app owner (accept stream-drop risk)
  │
Meeting starts
  │
  ├── meeting.rtms_started received within 60s?
  │     ├── YES → Connect RTMS WebSocket, begin recording
  │     └── NO  → Fall back to bot (or notify user RTMS unavailable)
  │
RTMS stream active
  │
  ├── Stream healthy → Continue recording
  ├── Stream drops (WebSocket close)
  │     ├── Wait 30s for new meeting.rtms_started
  │     │     ├── New webhook → Reconnect, log gap
  │     │     └── No webhook → Finalize partial recording; optionally deploy bot for remainder
  │     └── meeting.rtms_stopped → Finalize recording
  │
Meeting ends → Finalize all recordings

Recommendation: For high-stakes meetings (customer calls, compliance-required recordings), keep bot infrastructure available as a fallback during your RTMS rollout. Once you have confidence in RTMS reliability for your user base, you can decommission bots.

Run the Sample

Arlo  demonstrates the complete RTMS pattern with transcript capture, AI processing, and in-meeting display.

PlatformWhat you get
Deploy to Render Backend, frontend, RTMS service, managed database
Deploy to Railway Backend, frontend, RTMS service, managed database
Local setup
  1. Clone and configure:

    git clone https://github.com/zoom/arlo.git && cd arlo && cp .env.example .env
    
  2. Start ngrok with a static domain:

    ngrok http 3000 --domain=your-subdomain.ngrok-free.app
    
  3. Create a Zoom General App at marketplace.zoom.us :

    • Copy Client ID and Client Secret
    • Set OAuth Redirect URL to https://YOUR-NGROK-URL/api/auth/callback
    • Enable RTMS under Features > Zoom App SDK > Real-Time Media Streams > Transcripts
    • Add Event Subscription for meeting.rtms_started and meeting.rtms_stopped
  4. Fill in .env with your credentials

  5. Start the services:

    docker-compose up --build
    

App Manifest

The migration requires updating your Zoom App configuration from Meeting SDK to RTMS.

Required Scopes

ScopePurpose
meeting:read:meetingMeeting metadata
user:readUser profile for personalization

Note: RTMS access is configured via Zoom App SDK settings, not OAuth scopes.

RTMS Configuration

  1. In your app settings, go to Features > Zoom App SDK
  2. Enable Real-Time Media Streams
  3. Select the media types you need:
    • Transcripts: Speech-to-text segments
    • Audio: PCM audio stream
    • Video: H.264 video frames
    • Screen Share: Screen capture

Event Subscriptions

EventWhen it firesYour action
meeting.rtms_startedUser starts RTMS or auto-start triggersConnect to WebSocket
meeting.rtms_stoppedStream ends (host stops, meeting ends, etc.)Finalize recording

Auto-Start Configuration

For hands-free recording (no user action required each meeting):

  1. Go to Features > Zoom App SDK > Real-Time Media Streams
  2. Enable Auto-start
  3. Optionally disable Host approval required (admin setting)

With auto-start enabled, RTMS begins automatically when the app owner's meeting starts.

Enterprise Deployment (Admin Install)

For enterprise customers, individual OAuth flows per user create friction. Zoom supports admin-level installation where an admin installs the app for their organization, and users can start using it without individual consent flows.

Admin install flow

  1. Admin enables RTMS at the organization level

    • Zoom Admin Portal > Settings > "Allow apps to access real-time meeting content"
    • This is the master switch. If disabled, RTMS will not work for any meeting hosted by this organization.
  2. Admin installs your app for the organization

    • Admin navigates to your app's Zoom Marketplace listing (or a direct install link you provide)
    • Admin approves OAuth scopes for the organization
    • Admin selects which users or groups the app is installed for (all users or a subset)
  3. Admin configures RTMS settings

    • Enable auto-start for your app (Zoom Admin > Zoom Apps Settings)
    • Optionally disable host approval requirement (recommended for seamless recording)
    • Optionally enable "Allow meeting to start without host" (required if non-hosts need to trigger recording)

Post-install behavior

  • First meeting records automatically: No user login or app interaction required. When a meeting starts for an installed user, Zoom sends meeting.rtms_started to your webhook.
  • No bot joins: Participants see no additional attendee. Your app is listed in the meeting's Apps panel but does not appear as a participant.
  • Works for non-hosts: A user who is not the host can trigger RTMS via auto-start as long as they are an app owner (the app is installed for them).

Required Zoom account settings

SettingRequired ValueWhere to Configure
Allow apps to access real-time meeting contentEnabledZoom Admin > Settings
Your RTMS app installedYes, for target usersZoom Marketplace
Auto-start for your RTMS appEnabledZoom Admin > Zoom Apps Settings
Host approval for RTMSDisabled (recommended)Zoom Admin > Settings
Allow meeting to start without hostEnabled (if needed)Zoom Admin > Meeting Settings
Zoom client version6.5.5+All users

Compliance Considerations

RTMS is receive-only streaming, not Zoom's cloud recording. This distinction affects compliance requirements.

No native recording indicator

RTMS does not trigger Zoom's recording indicator (the red dot or "Recording" badge). From Zoom's perspective, RTMS streams data to your application; it is not "recording." If your jurisdiction requires visible notification that recording is active, you must implement this yourself.

Options for recording notification

OptionProsCons
Zoom Surface AppIn-meeting UI; can show recording status, consent dialog, cancel buttonRequires building a Zoom App with the Zoom Apps SDK
Account-level disclaimer popupZoom-native; no development requiredSame disclaimer for all apps on the account; not customizable per-app or per-meeting
Pre-meeting email/calendarSimple to implement; reaches external participantsNo in-meeting cancel capability; participants may not read it
Post-join external notificationCan include links to cancel/consent pageDisruptive (email/SMS during meeting); may be too late

Recommendation: If compliance requires in-meeting notification, build a Zoom Surface App. The Surface App and your RTMS General App can share the same Zoom App registration. The Surface App can display recording status, consent text, and a cancel button. It can also use the Zoom Apps SDK sendMessage() method to send chat messages on behalf of the app.

Pause, resume, and cancel recording

RTMS streams continuously while active. To implement pause/resume or cancel functionality:

  • Pause/Resume: Your app stops processing the stream but keeps the WebSocket open. Mark pause/resume timestamps and strip paused sections in post-processing. Alternatively, close the WebSocket on pause and wait for a new meeting.rtms_started on resume (but this is less reliable).
  • Cancel: Close the WebSocket connection and discard captured media. The stream will stop. If you need to restart, the user must manually trigger RTMS again (or wait for conditions to re-trigger auto-start, which may not happen mid-meeting).

A Surface App provides the best UX for these controls, with pause/cancel buttons visible in-meeting.

External participants (guests not in the host's organization) cannot see your app in their Apps panel unless they also install it. For consent:

  1. Use Zoom's account-level disclaimer (if the host's org has it configured)
  2. Send pre-meeting email with recording notice
  3. Build a Surface App with guest mode that displays consent to all participants

Acceptance Criteria

Use this checklist to verify your migration:

Webhook and connection:

  • Webhook endpoint validates signatures with timing-safe comparison
  • Webhook endpoint enforces 5-minute replay window
  • Duplicate rtms_stream_id webhooks are ignored
  • WebSocket handlers registered before join() call
  • Stream cleanup runs on onLeave and meeting.rtms_stopped
  • Multiple app owners in same meeting deduplicated by meeting_uuid

Data model:

  • Session data model updated with RTMS identifiers (rtms_stream_id, meeting_uuid)
  • Stream status tracking implemented (waiting, connected, ended)

Lifecycle and reliability:

  • Fallback logic implemented for when RTMS is unavailable (if applicable)
  • Stream persistence strategy defined for long meetings (scheduler-as-app-owner pattern)
  • Gap detection and logging implemented for mid-meeting stream drops
  • Reconnection handling tested (new meeting.rtms_started after drop)

Compliance (if required by your use case):

  • Recording notification mechanism implemented (Surface App, disclaimer, or pre-meeting email)
  • Pause/resume functionality tested (if offered to users)
  • Cancel recording functionality tested (WebSocket close, media discard)
  • External participant consent flow documented

User experience:

  • Chat functionality replaced with REST API, Surface App, or pre-meeting notification
  • Users no longer see bot participant in meetings
  • Admin install flow documented for enterprise customers

Decommissioning:

  • Bot infrastructure can be decommissioned after migration validation (or retained as fallback)