# RTMS - Webhooks RTMS-related webhook events and configuration. ## CRITICAL: Respond 200 IMMEDIATELY! **The #1 cause of random disconnects:** If your webhook handler takes too long to respond, Zoom assumes failure and retries. The retry creates a second connection, which kicks out your first connection (only 1 connection allowed per stream). ```javascript // CORRECT: Respond first, process async app.post('/webhook', (req, res) => { res.status(200).send(); // IMMEDIATELY! // Then process asynchronously handleRTMSEvent(req.body); }); // WRONG: Processing before responding app.post('/webhook', async (req, res) => { await heavyProcessing(req.body); // Zoom may retry while waiting! res.status(200).send(); }); ``` ## URL Validation Challenge When configuring your webhook URL, Zoom sends a validation challenge: ```javascript app.post('/webhook', (req, res) => { const { event, payload } = req.body; // Handle URL validation if (event === 'endpoint.url_validation') { const hash = crypto .createHmac('sha256', process.env.ZOOM_SECRET_TOKEN) .update(payload.plainToken) .digest('hex'); return res.json({ plainToken: payload.plainToken, encryptedToken: hash }); } res.status(200).send(); // ... handle other events }); ``` ## Events ### meeting.rtms_started Sent when RTMS stream is ready for a meeting. ```json { "event": "meeting.rtms_started", "payload": { "account_id": "account_id", "object": { "meeting_id": "meeting_id", "meeting_uuid": "meeting_uuid", "host_id": "host_user_id", "rtms_stream_id": "stream_id", "server_urls": "wss://rtms-sjc1.zoom.us/...", "signature": "auth_signature" } } } ``` ### meeting.rtms_stopped Sent when RTMS stream ends. ```json { "event": "meeting.rtms_stopped", "payload": { "account_id": "account_id", "object": { "meeting_id": "meeting_id", "rtms_stream_id": "stream_id" } } } ``` ### webinar.rtms_started Sent when RTMS stream is ready for a webinar. ```json { "event": "webinar.rtms_started", "payload": { "account_id": "account_id", "object": { "meeting_id": "meeting_id", "meeting_uuid": "meeting_uuid", "host_id": "host_user_id", "rtms_stream_id": "stream_id", "server_urls": "wss://rtms-sjc1.zoom.us/...", "signature": "auth_signature" } } } ``` > **Important**: Webinar payloads use `meeting_uuid`, NOT `webinar_uuid`. The signature and connection flow are identical to meetings. **Webinar-specific considerations:** - **Panelists**: Full audio/video streams are available for panelists. - **Attendees**: View-only participants; individual streams may not be available. - **Practice sessions**: Not documented for RTMS. - **Q&A/Polls**: Not exposed via RTMS. ### webinar.rtms_stopped Sent when RTMS stream ends for a webinar. ```json { "event": "webinar.rtms_stopped", "payload": { "account_id": "account_id", "object": { "meeting_id": "meeting_id", "rtms_stream_id": "stream_id" } } } ``` ### session.rtms_started Sent when RTMS stream is ready for a Video SDK session. ```json { "event": "session.rtms_started", "payload": { "account_id": "account_id", "object": { "session_id": "session_id", "rtms_stream_id": "stream_id", "server_urls": "wss://rtms-sjc1.zoom.us/...", "signature": "auth_signature" } } } ``` > **Important**: Video SDK payloads use `session_id` instead of `meeting_uuid`. The HMAC signature must use `session_id` in place of `meeting_uuid`. **Video SDK-specific considerations:** - Uses **SDK Key/Secret** (not OAuth Client ID/Secret) for authentication. - Requires a **Video SDK App** (not a General App) in Zoom Marketplace. - Once connected, the WebSocket protocol is identical to meetings. ### session.rtms_stopped Sent when RTMS stream ends for a Video SDK session. ```json { "event": "session.rtms_stopped", "payload": { "account_id": "account_id", "object": { "session_id": "session_id", "rtms_stream_id": "stream_id" } } } ``` ### Screen Share Events (via msg_type 5) Subscribe to receive `SHARING_START` and `SHARING_STOP` events when participants start/stop screen sharing. ## Payload Fields | Field | Description | |-------|-------------| | `rtms_stream_id` | Unique stream identifier | | `server_urls` | WebSocket signaling server URL | | `meeting_uuid` | Meeting unique identifier (needed for signature) | | `signature` | Pre-computed auth signature (alternative to self-generating) | ## Server URL Geo-Routing Server URLs contain airport/region codes: | Code | Location | |------|----------| | `sjc` | San Jose, California | | `iad` | Washington DC | | `sin` | Singapore | | `fra` | Frankfurt, Germany | | `syd` | Sydney, Australia | ```javascript // Extract region from server URL const hostname = new URL(serverUrl).hostname; // rtms-sjc1.zoom.us const region = hostname.split('-')[1].replace(/[0-9]/g, ''); // sjc ``` **Tip**: For production, route webhooks to workers in the same region as the Zoom server. ## Subscribing to RTMS Events ### In Zoom Marketplace (General App - Meetings and Webinars) 1. Go to your app settings 2. Navigate to **Features** → **Access** 3. **Enable Event Subscription** 4. Click **Add Event Subscription** 5. Enter your webhook endpoint URL 6. Search "rtms" and select: - `meeting.rtms_started` - `meeting.rtms_stopped` - `webinar.rtms_started` (if using webinars) - `webinar.rtms_stopped` (if using webinars) 7. Click **Done** then **Save** ### In Zoom Marketplace (Video SDK App) 1. Go to your Video SDK app settings 2. Add Event Subscription: - `session.rtms_started` - `session.rtms_stopped` ### Required Scopes **For Meetings** (Features → Scopes → Add Scopes → search "rtms"): | Scope | Purpose | |-------|---------| | `meeting:read:meeting_audio` | Access meeting audio | | `meeting:read:meeting_video` | Access meeting video | | `meeting:read:meeting_transcript` | Access transcripts | | `meeting:read:meeting_chat` | Access chat messages | **For Webinars** (add these in addition to meeting scopes): | Scope | Purpose | |-------|---------| | `webinar:read:webinar_audio` | Access webinar audio | | `webinar:read:webinar_video` | Access webinar video | | `webinar:read:webinar_transcript` | Access webinar transcripts | | `webinar:read:webinar_chat` | Access webinar chat messages | **For Video SDK**: Uses SDK Key/Secret credentials instead of OAuth scopes. ## Products Supporting RTMS | Product | Start Event | Stop Event | Payload ID | App Type | |---------|-------------|------------|------------|----------| | **Zoom Meetings** | `meeting.rtms_started` | `meeting.rtms_stopped` | `meeting_uuid` | General App | | **Zoom Webinars** | `webinar.rtms_started` | `webinar.rtms_stopped` | `meeting_uuid` (not webinar_uuid!) | General App | | **Zoom Video SDK** | `session.rtms_started` | `session.rtms_stopped` | `session_id` | Video SDK App | | Zoom Contact Center | `contactcenter.rtms_*` | `contactcenter.rtms_*` | See Zoom docs | Contact Center App | | Zoom Phone | `phone.rtms_*` | `phone.rtms_*` | See Zoom docs | General App | > **Key differences**: Meetings and webinars use a General App with OAuth credentials. Video SDK uses a Video SDK App with SDK Key/Secret. Once connected, the WebSocket protocol is identical across all products. ## Resources - **Event reference**: https://developers.zoom.us/docs/rtms/event-reference/ - **RTMS docs**: https://developers.zoom.us/docs/rtms/