Skip to navigation

MeetStream Guide: Zoom Meeting Direct Connect (RTMS)

View as Markdown

Record and transcribe a Zoom meeting without a bot joining it. Meeting Direct Connect uses Zoom’s Realtime Media Streams (RTMS): Zoom streams the meeting’s audio, video, screen share, transcript and participant events to MeetStream through your own Zoom app. No “notetaker” tile appears in the meeting, and there is no waiting room or recording-permission prompt.

A Direct Connect session is a normal MeetStream bot with platform: "ZoomRTMS". Every /api/v1/bots/{bot_id}/... endpoint you already use (status, audio, video, transcripts, participants, speaker timeline) works with its ID, and you receive the same signed webhooks.


How it works

  1. A meeting starts and RTMS starts for your Zoom app. The host can start it, or your Zoom account can start it automatically.
  2. Zoom sends a meeting.rtms_started webhook.
  3. MeetStream connects to the stream. There are two ways this happens:
    • Option A: you forward the webhook. Your server receives Zoom’s webhook, signs it, and calls POST /api/v1/meeting_direct_connect. Your Zoom client secret never leaves your server.
    • Option B: MeetStream receives the webhook. You register your Zoom app with MeetStream once and point the app’s Event Subscription at MeetStream. You don’t need your own server.
  4. MeetStream records until Zoom stops the stream, you call leave, or the maximum session length is reached. The usual post-call processing then runs, and you get bot.done.

Prerequisites

  • A Zoom General app with RTMS enabled, and these event subscriptions:
    • meeting.rtms_started
    • meeting.rtms_stopped
  • RTMS scopes on the app for the media you want, for example meeting:read:meeting_audio, meeting:read:meeting_video and meeting:read:meeting_transcript.
  • RTMS turned on for the meeting. Either the host starts it, or an account setting starts it automatically. Starting RTMS needs Zoom’s Developer Pack on the app owner’s Zoom account.
  • Your MeetStream API key.

Option A: Forward Zoom’s webhook

Endpoint

POST https://api.meetstream.ai/api/v1/meeting_direct_connect
ParameterLocationRequiredDescription
AuthorizationHeaderYesToken <YOUR_API_KEY>
zoom_rtmsBodyYesThe stream details from Zoom’s meeting.rtms_started webhook, plus your signature (see below)

zoom_rtms fields:

FieldDescription
meeting_uuidpayload.meeting_uuid from Zoom’s webhook
rtms_stream_idpayload.rtms_stream_id from Zoom’s webhook
server_urlspayload.server_urls from Zoom’s webhook (a string or an array). Each URL must be a wss:// URL on zoom.us or zoomgov.com
signatureHMAC-SHA256(client_secret, "client_id,meeting_uuid,rtms_stream_id") as 64 hex characters, using your Zoom app’s client ID and secret

Optional fields

FieldDescription
bot_nameA label for the session in your dashboard. It is not shown in the meeting
callback_urlWhere to send status webhooks for this session
custom_attributesYour own key/value data, returned with the session
recording_config.transcript.providerPost-call transcription: deepgram, assemblyai, jigsawstack, sarvam or meetstream. Use meeting_captions for Zoom’s own transcript
live_transcription_required.webhook_urlLive transcript delivery. Requires meeting_captions
video_requiredtrue records the active-speaker video and screen shares
video_separate_streamstrue splits the active-speaker video into a file per participant. Requires video_required
audio_separate_streamstrue records separate audio for each participant
audio_separate_mp3true adds MP3 copies of the per-participant audio. Requires audio_separate_streams
live_audio_required.websocket_urlLive audio over a WebSocket (ws:// or wss://)
live_video_required.websocket_urlLive video over a WebSocket (ws:// or wss://)
automatic_leave.in_call_recording_timeoutMaximum session length in seconds: 600 to 18000. The default is 14400 (4 hours)

These fields are not accepted, because no bot joins the meeting: meeting_link, join_at, socket_connection_url, agents, video_layout, bot_message, bot_image_url and streaming transcription providers.

Example request

curl -X POST "https://api.meetstream.ai/api/v1/meeting_direct_connect" \
-H "Authorization: Token <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"zoom_rtms": {
"meeting_uuid": "4444AAAiAAAAAiAiAiiAii==",
"rtms_stream_id": "609340fc1e0b4f1b9e0a1d2c3b4a5f60",
"server_urls": "wss://example.zoom.us",
"signature": "<64 hex characters>"
},
"callback_url": "https://your-app.example.com/meetstream",
"recording_config": {
"transcript": {"provider": {"meeting_captions": {}}}
}
}'

Response (201)

{
"id": "6f0c2a1e-...",
"bot_id": "6f0c2a1e-...",
"platform": "ZoomRTMS",
"status": {"code": "joining", "bot_status": "Joining"},
"zoom_rtms": {
"meeting_uuid": "4444AAAiAAAAAiAiAiiAii==",
"rtms_stream_id": "609340fc1e0b4f1b9e0a1d2c3b4a5f60"
},
"transcript_id": "...",
"created_at": "2026-11-09T10:00:00Z"
}

id and bot_id are the same value. Use it with any /api/v1/bots/{bot_id}/... endpoint.

Example: forwarding server (Python, Flask)

import hashlib
import hmac
import os
import requests
from flask import Flask, request
app = Flask(__name__)
CLIENT_ID = os.environ["ZOOM_CLIENT_ID"]
CLIENT_SECRET = os.environ["ZOOM_CLIENT_SECRET"]
@app.post("/zoom/webhook")
def zoom_webhook():
event = request.get_json()
# Also verify Zoom's x-zm-signature header and answer endpoint.url_validation (see Zoom's docs).
if event.get("event") != "meeting.rtms_started":
return "", 200
p = event["payload"]
message = f"{CLIENT_ID},{p['meeting_uuid']},{p['rtms_stream_id']}"
signature = hmac.new(CLIENT_SECRET.encode(), message.encode(), hashlib.sha256).hexdigest()
requests.post(
"https://api.meetstream.ai/api/v1/meeting_direct_connect",
headers={"Authorization": f"Token {os.environ['MEETSTREAM_API_KEY']}"},
json={
"zoom_rtms": {
"meeting_uuid": p["meeting_uuid"],
"rtms_stream_id": p["rtms_stream_id"],
"server_urls": p["server_urls"],
"signature": signature,
},
"recording_config": {"transcript": {"provider": {"meeting_captions": {}}}},
},
timeout=5,
)
return "", 200

Example: forwarding server (Node.js, Express)

import crypto from "node:crypto";
import express from "express";
const app = express().use(express.json());
const { ZOOM_CLIENT_ID, ZOOM_CLIENT_SECRET, MEETSTREAM_API_KEY } = process.env;
app.post("/zoom/webhook", async (req, res) => {
// Also verify Zoom's x-zm-signature header and answer endpoint.url_validation (see Zoom's docs).
if (req.body.event !== "meeting.rtms_started") return res.sendStatus(200);
const { meeting_uuid, rtms_stream_id, server_urls } = req.body.payload;
const signature = crypto
.createHmac("sha256", ZOOM_CLIENT_SECRET)
.update(`${ZOOM_CLIENT_ID},${meeting_uuid},${rtms_stream_id}`)
.digest("hex");
await fetch("https://api.meetstream.ai/api/v1/meeting_direct_connect", {
method: "POST",
headers: { Authorization: `Token ${MEETSTREAM_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
zoom_rtms: { meeting_uuid, rtms_stream_id, server_urls, signature },
recording_config: { transcript: { provider: { meeting_captions: {} } } },
}),
});
res.sendStatus(200);
});
app.listen(8080);

Answer Zoom within 3 seconds. If Zoom doesn’t get a quick 200, it retries the webhook. MeetStream rejects a duplicate for a stream that is already active with 409, so a retry never starts a second session.


Option B: Let MeetStream receive Zoom’s webhook

Register your Zoom app once. This is the same app registry used for Zoom OAuth bots (see the Zoom OAuth Meeting Bots guide). Add an rtms block with the defaults for every session the app starts:

curl -X POST "https://api.meetstream.ai/api/v1/zoom/oauth/apps" \
-H "Authorization: Token <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"client_id": "<ZOOM_CLIENT_ID>",
"client_secret": "<ZOOM_CLIENT_SECRET>",
"webhook_secret": "<ZOOM_SECRET_TOKEN>",
"label": "Acme RTMS",
"kind": "user_level",
"rtms": {
"enabled": true,
"video_required": true,
"callback_url": "https://your-app.example.com/meetstream",
"recording_config": {"transcript": {"provider": {"meeting_captions": {}}}}
}
}'

The response includes zoom_oauth_app_id and webhook_url. In your Zoom app, set the Event Subscription URL to that webhook_url and subscribe to meeting.rtms_started and meeting.rtms_stopped.

From then on, MeetStream checks Zoom’s signature on each webhook and starts a session for every meeting.rtms_started. Each session’s custom_attributes include zoom_oauth_app_id, zoom_meeting_id and zoom_operator_id, so you can match it to your meeting.

  • Change the defaults: PATCH /api/v1/zoom/oauth/apps/{zoom_oauth_app_id} with a new rtms block.
  • Stop starting sessions for the app: PATCH with {"rtms": {"enabled": false}}.

The rtms block accepts enabled, bot_name, callback_url, custom_attributes, recording_config, live_transcription_required, live_audio_required, live_video_required, video_required, video_separate_streams, audio_separate_streams, audio_separate_mp3 and automatic_leave. callback_url must be an https:// URL.


Get a session

GET https://api.meetstream.ai/api/v1/meeting_direct_connect/{id}

Returns the session’s status, its status history, and its recordings with download links:

{
"id": "6f0c2a1e-...",
"bot_id": "6f0c2a1e-...",
"platform": "ZoomRTMS",
"status": {"code": "done", "bot_status": "Done", "updated_at": "..."},
"status_changes": [{"code": "joining", "bot_status": "Joining", "created_at": "..."}],
"recordings": [{
"id": "6f0c2a1e-...",
"started_at": "...",
"completed_at": "...",
"media_shortcuts": {
"audio_mixed": {"status": {"code": "done"}, "data": {"download_url": "https://...", "format": "wav"}},
"transcript": {"status": {"code": "done"}, "data": {"download_url": "https://...", "provider": "meeting_captions"}},
"participant_events": {"status": {"code": "done"}, "data": {"download_url": "https://..."}},
"video_mixed": {"status": {"code": "done"}, "data": {"download_url": "https://...", "format": "mp4"}}
}
}]
}

video_mixed appears only when the session was started with video_required: true. It shows processing until the video is ready.

status.codeMeaning
joiningConnecting to Zoom’s stream
in_meetingReceiving media
leavingThe stream is ending
stoppedThe stream ended
processingPost-call processing is running
doneRecordings and transcripts are ready
fatalThe session failed

To list all sessions, call GET /api/v1/bots?platform=ZoomRTMS.


End a session early

POST https://api.meetstream.ai/api/v1/meeting_direct_connect/{id}/leave

This stops recording and starts post-call processing. It works the same as POST /api/v1/bots/{bot_id}/remove_bot. Calling it on a session that has already finished returns 200 and keeps its final status.

A session also ends when the host stops RTMS, when the meeting ends, or when in_call_recording_timeout is reached.


Webhooks

Sessions send the same signed webhooks as bots (bot.inmeeting, bot.recording, bot.leaving, bot.stopped, bot.done and failure events) to your callback_url. participant_events.join and participant_events.leave are available as real-time events. Verify them the same way as other MeetStream webhooks (see the Webhook Signature Verification guide).


Limitations

  • Receive-only. Nothing can be sent into the meeting: no chat messages, images, audio playback, raise hand or MIA agents. send_message, send_image, send_command, pause_recording and resume_recording return 400 with code: "unsupported_on_zoom_rtms".
  • No chat capture yet. get_chats returns an empty list.
  • Participant events are join and leave only. Microphone, camera, host and email details are not available.
  • Video is the active speaker. Zoom streams one camera at a time, so video_separate_streams gives each participant’s video only while they are the active speaker.
  • Per-participant audio covers up to 3 people speaking at the same time, a Zoom limit.
  • Live transcription uses Zoom’s own transcript (meeting_captions). Streaming transcription providers are not supported.
  • No breakout rooms. RTMS streams the main meeting only.
  • The host stays in control. A Zoom admin can turn off RTMS during a meeting, and the session then ends.
  • Start-up time. A session can take up to about a minute to start receiving media.
  • US execution region only.

Error Responses (POST /api/v1/meeting_direct_connect)

StatusMeaning
400Invalid request: a missing zoom_rtms field, a server_urls host outside Zoom, a malformed signature, or an unsupported option. The error field says which.
401Missing or invalid API key.
403Meeting Direct Connect is not enabled for your account, or your balance is too low.
409A session for this rtms_stream_id is already active. The body includes the existing session’s id. After a session ends, the same stream ID can be used again, for example to retry with a corrected signature.
422Your account runs in a region other than the US.
503The session could not be started. Retry.

If the signature is wrong, the request is still accepted (201), but the session moves to fatal when Zoom rejects the connection. Check GET /api/v1/meeting_direct_connect/{id} or the failure webhook.