Skill 139 · Build Zoom Team Chat App
Subchapter 139.8
references/samples.mdMarkdown14 KBView on GitHub
Analysis of 10 official Zoom Team Chat sample applications, extracted patterns, and best practices.
| Sample | Language | Complexity | Best For |
|---|---|---|---|
| chatbot-nodejs-quickstart (opens in a new tab) | Node.js | ⭐ Beginner | Start here - Tutorial series |
| zoom-chatbot-claude-sample (opens in a new tab) | Node.js | ⭐⭐ Intermediate | LLM integration pattern |
| unsplash-chatbot (opens in a new tab) | Node.js | ⭐⭐ Intermediate | API integration + database |
| zoom-erp-chatbot-sample (opens in a new tab) | Node.js | ⭐⭐⭐ Advanced | Enterprise integration |
| task-manager-sample (opens in a new tab) | Node.js | ⭐⭐⭐ Advanced | Full CRUD application |
| zoom-cohere-chatbot-sample (opens in a new tab) | Node.js | ⭐⭐ Intermediate | Cohere LLM integration |
| zoom-cerebras-chatbot-sample (opens in a new tab) | Node.js | ⭐⭐ Intermediate | Cerebras LLM integration |
| zoom-team-chat-shortcut-sample (opens in a new tab) | Node.js | ⭐⭐ Intermediate | Shortcuts and UI elements |
| zoom-teams-chat-snowflake-sample (opens in a new tab) | Node.js | ⭐⭐⭐ Advanced | Snowflake data integration |
| rivet-javascript-sample (opens in a new tab) | Node.js | ⭐⭐ Intermediate | Rivet SDK usage |
Description: Official tutorial series covering 9 episodes from setup to advanced features.
Key Features:
Project Structure:
chatbot-nodejs-quickstart/
├── routes/
│ ├── zoom-webhookHandler.js # Webhook event handling
│ └── oauth-routes.js # OAuth flow
├── utils/
│ ├── zoom-api.js # API helper functions
│ ├── zoom-chatbot-auth.js # Token generation
│ └── validation.js # Webhook signature verification
├── views/ # EJS templates
├── server.js # Express app
└── .env.example # Environment variablesKey Patterns:
async function handleZoomWebhook(req, res) {
verifyZoomWebhookSignature(req);
const { event, payload } = req.body;
switch (event) {
case 'bot_notification':
return handleBotNotification(payload, res);
case 'interactive_message_actions':
return handleButtonClick(payload, res);
// ... more cases
}
}async function getChatbotToken() {
const credentials = Buffer.from(
`${CLIENT_ID}:${CLIENT_SECRET}`
).toString('base64');
const response = await fetch('https://zoom.us/oauth/token', {
method: 'POST',
headers: { 'Authorization': `Basic ${credentials}` },
body: 'grant_type=client_credentials'
});
return (await response.json()).access_token;
}Best Practices:
Recommended For: First-time chatbot developers
Description: AI-powered chatbot using Anthropic Claude for natural language responses.
Key Features:
LLM Integration Pattern:
case 'bot_notification': {
const { toJid, cmd, accountId } = payload;
// Call Claude API
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
messages: [{ role: 'user', content: cmd }]
});
const llmResponse = response.content[0].text;
// Send back to Zoom
await sendChatbotMessage(toJid, accountId, {
body: [{ type: 'message', text: llmResponse }]
});
}Conversation History Pattern:
const conversationHistory = new Map();
function addToHistory(userId, role, content) {
if (!conversationHistory.has(userId)) {
conversationHistory.set(userId, []);
}
conversationHistory.get(userId).push({ role, content });
}
// In bot_notification handler
const history = conversationHistory.get(userId) || [];
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
messages: history
});Environment Variables:
ANTHROPIC_API_KEY=your_api_key_here
ZOOM_CLIENT_ID=...
ZOOM_CLIENT_SECRET=...
ZOOM_BOT_JID=...Recommended For: Building AI assistants
Description: Image search bot integrating Unsplash API with database storage.
Key Features:
Database Schema:
CREATE TABLE users (
id INTEGER PRIMARY KEY,
zoom_user_id TEXT UNIQUE,
preferences TEXT
);
CREATE TABLE searches (
id INTEGER PRIMARY KEY,
user_id INTEGER,
query TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);Image Display Pattern:
{
"content": {
"head": { "text": "Image Results" },
"body": [
{
"type": "attachments",
"img_url": imageData.urls.regular,
"resource_url": imageData.links.html,
"information": {
"title": { "text": imageData.description },
"description": { "text": `Photo by ${imageData.user.name}` }
}
}
]
}
}Best Practices:
Recommended For: External API integration patterns
Description: Enterprise Resource Planning integration with scheduled alerts.
Key Features:
Scheduled Alerts Pattern:
const cron = require('node-cron');
// Daily report at 9 AM
cron.schedule('0 9 * * *', async () => {
const report = await getERPReport();
await sendChatbotMessage(channelJid, accountId, {
head: { "text": "Daily ERP Report" },
body: [
{ "type": "fields", "items": report.fields },
{
"type": "actions",
"items": [
{ "text": "View Details", "value": "view_report" }
]
}
]
});
});Approval Workflow Pattern:
// Send approval request
{
"head": { "text": "Expense Approval Required" },
"body": [
{ "type": "fields", "items": expenseFields },
{
"type": "actions",
"items": [
{ "text": "Approve", "value": `approve_${expenseId}`, "style": "Primary" },
{ "text": "Reject", "value": `reject_${expenseId}`, "style": "Danger" }
]
}
]
}
// Handle button click
case 'interactive_message_actions': {
const action = payload.actionItem.value;
const [decision, expenseId] = action.split('_');
await updateERPStatus(expenseId, decision);
await sendConfirmation(payload.toJid, decision);
}Recommended For: Enterprise integrations, workflows
Description: Full-featured task management application with CRUD operations.
Key Features:
CRUD Pattern:
// CREATE
case 'bot_notification': {
if (cmd.startsWith('create task')) {
const taskData = parseTaskCommand(cmd);
const task = await db.createTask(taskData);
await sendTaskCreatedMessage(toJid, accountId, task);
}
}
// READ
case 'interactive_message_actions': {
if (actionItem.value.startsWith('view_task')) {
const taskId = actionItem.value.split('_')[2];
const task = await db.getTask(taskId);
await sendTaskDetails(toJid, accountId, task);
}
}
// UPDATE
case 'interactive_message_actions': {
if (actionItem.value.startsWith('complete_task')) {
const taskId = actionItem.value.split('_')[2];
await db.updateTaskStatus(taskId, 'completed');
await sendStatusUpdate(toJid, accountId, taskId);
}
}
// DELETE
case 'interactive_message_actions': {
if (actionItem.value.startsWith('delete_task')) {
const taskId = actionItem.value.split('_')[2];
await db.deleteTask(taskId);
await sendDeletionConfirmation(toJid, accountId, taskId);
}
}Recommended For: Full application architecture
All samples use .env files with similar structure:
# Authentication
ZOOM_CLIENT_ID=
ZOOM_CLIENT_SECRET=
ZOOM_BOT_JID=
ZOOM_VERIFICATION_TOKEN=
ZOOM_ACCOUNT_ID=
# Third-party APIs (if applicable)
ANTHROPIC_API_KEY=
UNSPLASH_ACCESS_KEY=
# Server
PORT=4000
NODE_ENV=developmentCommon folder organization:
sample-app/
├── routes/
│ ├── webhook.js # Webhook handlers
│ └── oauth.js # OAuth flows (if needed)
├── utils/
│ ├── zoom-api.js # Zoom API wrappers
│ ├── auth.js # Token management
│ └── validation.js # Input validation
├── models/ # Database models (if applicable)
├── views/ # Frontend templates (if applicable)
├── server.js # Express app
├── .env.example
└── package.jsonAll samples verify webhook signatures:
function verifyWebhook(req) {
const signature = req.headers['x-zm-signature'];
const timestamp = req.headers['x-zm-request-timestamp'];
const message = `v0:${timestamp}:${JSON.stringify(req.body)}`;
const hash = crypto.createHmac('sha256', SECRET_TOKEN)
.update(message)
.digest('hex');
return signature === `v0=${hash}`;
}Consistent error handling pattern:
app.post('/webhook', async (req, res) => {
try {
verifyWebhook(req);
await handleWebhook(req.body);
res.status(200).json({ success: true });
} catch (error) {
console.error('Webhook error:', error);
if (error.message.includes('signature')) {
return res.status(401).json({ error: 'Invalid signature' });
}
res.status(500).json({ error: 'Internal server error' });
}
});Respond immediately, process async:
app.post('/webhook', (req, res) => {
// Respond immediately
res.status(200).json({ success: true });
// Process asynchronously
processWebhookAsync(req.body).catch(error => {
console.error('Async processing error:', error);
});
});Common lifecycle across all samples:
1. User Action (slash command, button click, message)
↓
2. Zoom sends webhook to Bot Endpoint URL
↓
3. Server verifies signature
↓
4. Server responds 200 (immediately)
↓
5. Server processes request (async)
↓
6. Server calls external APIs if needed
↓
7. Server sends chatbot message back to ZoomSimple bots: In-memory state (Map/Object) Production bots: Database (PostgreSQL, MongoDB, Redis)
// Simple (development)
const userState = new Map();
// Production
const userState = {
async get(userId) {
return await db.query('SELECT * FROM user_state WHERE user_id = $1', [userId]);
},
async set(userId, state) {
return await db.query('INSERT INTO user_state (user_id, state) VALUES ($1, $2) ON CONFLICT (user_id) DO UPDATE SET state = $2', [userId, state]);
}
};Some samples may use deprecated patterns:
// Hardcoded credentials
const CLIENT_ID = 'abc123';// Environment variables
const CLIENT_ID = process.env.ZOOM_CLIENT_ID;// Synchronous webhook processing (may timeout)
app.post('/webhook', async (req, res) => {
await longRunningProcess();
res.status(200).json({ success: true });
});// Async processing
app.post('/webhook', (req, res) => {
res.status(200).json({ success: true });
longRunningProcess().catch(console.error);
});