Setting the file. One moment.
Skill 139 · Build Zoom Team Chat App
Subchapter 139.20
examples/chatbot-setup.mdMarkdown12 KBView on GitHub
Build your first interactive Zoom chatbot from scratch. This guide provides complete, production-ready code.
my-zoom-chatbot/
├── .env
├── .env.example
├── package.json
├── server.js
├── routes/
│ └── webhook.js
└── utils/
├── auth.js
├── chatbot.js
└── validation.jsmkdir my-zoom-chatbot
cd my-zoom-chatbot
npm init -ynpm install express dotenv node-fetch# .env
ZOOM_CLIENT_ID=your_client_id_here
ZOOM_CLIENT_SECRET=your_client_secret_here
ZOOM_BOT_JID=v1abc123xyz@xmpp.zoom.us
ZOOM_VERIFICATION_TOKEN=your_webhook_secret_token
ZOOM_ACCOUNT_ID=your_account_id
PORT=4000// utils/auth.js
const fetch = require('node-fetch');
/**
* Get chatbot access token using client_credentials flow
*/
async function getChatbotToken() {
const credentials = Buffer.from(
`${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}`
).toString('base64');
const response = await fetch('https://zoom.us/oauth/token', {
method: 'POST',
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'grant_type=client_credentials'
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Token error: ${error.error_description || error.error}`);
}
const data = await response.json();
return data.access_token;
}
module.exports = { getChatbotToken };// utils/validation.js
const crypto = require('crypto');
/**
* Verify Zoom webhook signature
*/
function verifyZoomWebhookSignature(req) {
const signature = req.headers['x-zm-signature'];
const timestamp = req.headers['x-zm-request-timestamp'];
if (!signature || !timestamp) {
throw new Error('Missing signature headers');
}
const message = `v0:${timestamp}:${JSON.stringify(req.body)}`;
const hash = crypto
.createHmac('sha256', process.env.ZOOM_VERIFICATION_TOKEN)
.update(message)
.digest('hex');
if (signature !== `v0=${hash}`) {
throw new Error('Invalid webhook signature');
}
return true;
}
/**
* Sanitize message (4096 char limit)
*/
function sanitizeMessage(message) {
if (typeof message !== 'string') return '';
return message
.trim()
.replace(/[\x00-\x1F\x7F]/g, '')
.substring(0, 4096);
}
/**
* Validate JID format
*/
function isValidJID(jid) {
if (typeof jid !== 'string' || !jid.trim()) return false;
return /^[^@\s]+@[^@\s]+$/.test(jid);
}
module.exports = {
verifyZoomWebhookSignature,
sanitizeMessage,
isValidJID
};// utils/chatbot.js
const fetch = require('node-fetch');
const { getChatbotToken } = require('./auth');
const { sanitizeMessage } = require('./validation');
/**
* Send chatbot message
*/
async function sendChatbotMessage(toJid, accountId, content) {
const accessToken = await getChatbotToken();
const body = {
robot_jid: process.env.ZOOM_BOT_JID,
to_jid: toJid,
account_id: accountId,
content: content
};
const response = await fetch('https://api.zoom.us/v2/im/chat/messages', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Send message error: ${JSON.stringify(error)}`);
}
return response.json();
}
/**
* Send simple text message
*/
async function sendTextMessage(toJid, accountId, text) {
return sendChatbotMessage(toJid, accountId, {
body: [
{ type: 'message', text: sanitizeMessage(text) }
]
});
}
/**
* Send message with buttons
*/
async function sendMessageWithButtons(toJid, accountId, options) {
const { title, message, buttons } = options;
return sendChatbotMessage(toJid, accountId, {
head: {
text: title
},
body: [
{ type: 'message', text: sanitizeMessage(message) },
{
type: 'actions',
items: buttons.map(btn => ({
text: btn.text,
value: btn.value,
style: btn.style || 'Default'
}))
}
]
});
}
/**
* Send message with fields
*/
async function sendMessageWithFields(toJid, accountId, options) {
const { title, fields } = options;
return sendChatbotMessage(toJid, accountId, {
head: {
text: title
},
body: [
{
type: 'fields',
items: fields.map(field => ({
key: field.key,
value: field.value
}))
}
]
});
}
module.exports = {
sendChatbotMessage,
sendTextMessage,
sendMessageWithButtons,
sendMessageWithFields
};// routes/webhook.js
const crypto = require('crypto');
const { verifyZoomWebhookSignature } = require('../utils/validation');
const { sendTextMessage, sendMessageWithButtons } = require('../utils/chatbot');
async function handleWebhook(req, res) {
try {
// Verify signature
// server.js
require('dotenv').config();
const express = require('express');
const { handleWebhook } = require('./routes/webhook');
const app = express();
const PORT = process.env.PORT || 4000;
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Routes
app.get('/', (req, res) => {
res.json({ message: 'Zoom Team Chat Bot is running!' });
});
app.post('/webhook', handleWebhook);
// Start server
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
console.log(`Webhook endpoint: ${process.env.PUBLIC_BASE_URL || 'https://YOUR_PUBLIC_BASE_URL'}/webhook`);
});# Install ngrok
npm install -g ngrok
# Start your server
node server.js
# In a new terminal, expose with ngrok
ngrok http 4000
# Copy the HTTPS URL (e.g., https://abc123.ngrok.io)https://abc123.ngrok.io/webhook/mybotZoom will send a endpoint.url_validation request. If successful, you’ll see a green checkmark.
/mybot helpYou should see the bot respond with the help message!
/mybot help - Shows help message/mybot ping - Responds with “Pong! 🏓”/mybot demo - Shows buttons# Production .env
ZOOM_CLIENT_ID=your_production_client_id
ZOOM_CLIENT_SECRET=your_production_client_secret
ZOOM_BOT_JID=v1abc123xyz@xmpp.zoom.us # Production Bot JID
ZOOM_VERIFICATION_TOKEN=your_production_token
ZOOM_ACCOUNT_ID=your_account_id
PORT=4000
NODE_ENV=productionOptions:
Requirements:
| Issue | Solution |
|---|---|
| “Invalid signature” | Verify ZOOM_VERIFICATION_TOKEN matches Zoom Marketplace |
| Bot doesn’t respond | Check ngrok is running and URL is correct |
| URL validation fails | Ensure endpoint returns plainToken + encryptedToken |
| Messages not sending | Verify Bot JID and Account ID are correct |