Stream real-time Zoom meeting transcripts to multiple AI models via OpenRouter and synthesize a unified response.
Built with RTMSManager - Zoom's JavaScript library for real-time media streaming.
npm install
cp .env.example .env # Fill in your credentials
node index.jsExpose with ngrok: ngrok http 3000
This sample captures live transcript data from Zoom meetings via RTMS and sends each transcript segment to multiple AI models simultaneously through OpenRouter's unified API. It then synthesizes the responses from all models into a single, well-validated answer using a designated synthesis model. This multi-model approach helps cross-check facts and generate more accurate responses.
- Node.js v18+
- Zoom account with RTMS enabled
- OpenRouter API key
- ngrok for local development
| Variable | Required | Description |
|---|---|---|
ZOOM_SECRET_TOKEN |
Yes | Secret token for webhook URL validation |
ZOOM_CLIENT_ID |
Yes | Your Zoom app's Client ID |
ZOOM_CLIENT_SECRET |
Yes | Your Zoom app's Client Secret |
PORT |
No | Server port (default: 3000) |
WEBHOOK_PATH |
No | Webhook endpoint path (default: /webhook) |
OPENROUTER_API_KEY |
Yes | Your OpenRouter API key |
OPENROUTER_MODELS |
No | Comma-separated list of models to query (default: x-ai/grok-4.1-fast) |
OPENROUTER_SYNTHESIS_MODEL |
No | Model used to synthesize final answer (default: x-ai/grok-4.1-fast) |
const rtmsConfig = {
logging: {
enabled: true,
logDir: path.join(__dirname, 'logs'),
console: true
},
mediaSocketConnectionMode: process.env.MEDIA_SOCKET_CONNECTION_MODE || 'split',
mediaTypes: RTMSManager.MEDIA.TRANSCRIPT,
credentials: {
meeting: {
clientId: process.env.ZOOM_CLIENT_ID,
clientSecret: process.env.ZOOM_CLIENT_SECRET,
zoomSecretToken: process.env.ZOOM_SECRET_TOKEN,
}
},
mediaParams: {
transcript: {
contentType: MEDIA_PARAMS.MEDIA_CONTENT_TYPE_TEXT,
language: MEDIA_PARAMS.LANGUAGE_ID_ENGLISH,
}
}
};
await RTMSManager.init(rtmsConfig);const webhookManager = new WebhookManager({
config: {
webhookPath: process.env.WEBHOOK_PATH || '/webhook',
zoomSecretToken: rtmsConfig.credentials.meeting.zoomSecretToken,
},
app: app
});
webhookManager.on('event', (event, payload) => {
console.log('[send_to_openrouter] Webhook Event:', event);
RTMSManager.handleEvent(event, payload);
});
webhookManager.setup();RTMSManager.on('transcript', async ({ text, userId, userName, timestamp, meetingId }) => {
console.log(`[TRANSCRIPT] ${userName}: ${text}`);
await contextualSynthesisFromMultipleModels(text);
});
RTMSManager.on('meeting.rtms_started', (payload) => {
console.log('[send_to_openrouter] RTMS Started:', payload.meeting_uuid);
});
RTMSManager.on('meeting.rtms_stopped', (payload) => {
console.log('[send_to_openrouter] RTMS Stopped:', payload.meeting_uuid);
});await RTMSManager.start();
server.listen(appConfig.port, () => {
console.log(`[send_to_openrouter] Server listening on port ${appConfig.port}`);
console.log(`[send_to_openrouter] Webhook endpoint: http://localhost:${appConfig.port}${process.env.WEBHOOK_PATH || '/webhook'}`);
});| File | Purpose |
|---|---|
index.js |
Main application entry point, sets up RTMSManager and webhook handling |
chatWithOpenrouter.js |
OpenRouter API integration with multi-model querying and response synthesis |
- Server starts and initializes RTMSManager with transcript media type (flag 32)
- WebhookManager listens for Zoom webhook events on the configured endpoint
- When a meeting with RTMS starts,
meeting.rtms_startedevent triggers connection setup - RTMSManager automatically handles WebSocket connections and authentication
- As participants speak, transcript events are emitted with text and speaker info
- Each transcript segment is sent to multiple AI models in parallel via OpenRouter
- The system waits for all model responses to complete
- A synthesis model receives all responses and generates a unified, validated answer
- The final synthesized response is logged to the console
- When the meeting ends,
meeting.rtms_stoppedcloses connections gracefully
No OpenRouter responses
- Verify your
OPENROUTER_API_KEYis valid - Check that the specified models are available on OpenRouter
- Review console logs for specific error messages
Connection issues
- Verify ngrok is running and the tunnel is active
- Check that Zoom app credentials in
.envare correct - Ensure the webhook endpoint is accessible from the internet
Slow synthesis responses
- The synthesis step waits for all models to respond before proceeding
- Consider using faster models or reducing the number of models queried
- The spinner shows elapsed time during synthesis for monitoring
Model-specific errors
- Some models may fail while others succeed; the system continues with available responses
- Check OpenRouter's model availability and pricing for your selected models
- RTMSManager Library Docs - Full API reference
- Zoom App Setup Guide - Configure your Zoom app
- Troubleshooting Guide - Common issues
The project sends RTMS transcript text to an OpenRouter-compatible model. Its multi-stage Dockerfile keeps build tooling out of the final runtime image and does not hard-code a CPU architecture.
Build and run it from the rtms-samples repository root:
docker build -f transcript/send_transcript_to_openrouter_js/Dockerfile -t rtms-transcript-send_transcript_to_openrouter_js .
docker run --rm --env-file transcript/send_transcript_to_openrouter_js/.env -p 3000:3000 rtms-transcript-send_transcript_to_openrouter_jsRun the build from the repository root because the Dockerfile uses repository-relative paths. Runtime secrets are supplied with --env-file and are excluded from the image build context.
Normal Zoom webhook deliveries are verified against the exact raw request body using
x-zm-signature and x-zm-request-timestamp. Configure ZOOM_SECRET_TOKEN with the
Marketplace app's webhook Secret Token. Requests with missing, invalid, or stale
signatures are rejected; the default replay window is 300 seconds and can be changed
with WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS.