Build Zoom Team Chat App
OpenAIOfficialCreated by ZoomDistributed by OpenAI
- Skill
- Tool integration
- Software development
- MIT
- Medium risk
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a systematic approach to building applications for Zoom Team Chat. It guides developers through selecting the appropriate API surface, configuring app settings, modeling message structures, and implementing secure authentication and webhook handling. It covers the lifecycle of building both user-scoped messaging integrations and interactive chatbot workflows.
What it does
- Helps select between Team Chat API and Chatbot API based on integration requirements.
- Outlines the configuration of app types, scopes, and role enablement.
- Provides guidance on modeling message structures including rich cards, forms, and slash commands.
- Details the implementation of OAuth flows and token management.
- Supports the setup of webhook handlers with signature verification.
- Assists in debugging common issues related to JID formats, channel membership, and payload shapes.
When to use it
- When you need to integrate custom messaging or automation into Zoom Team Chat.
- When designing interactive chatbot workflows with buttons, forms, or dropdowns.
- When setting up OAuth authentication for Zoom-based applications.
- When troubleshooting connectivity or message delivery issues in a Zoom integration.
Advantages
- Provides a clear, step-by-step path for Zoom app development.
- Ensures security best practices like signature verification are prioritized.
- Helps avoid common pitfalls in API selection and message formatting.
- Centralizes references for environment variables and API endpoints.
Usage examples
- “How do I start building a chatbot for Zoom Team Chat?”
- The agent will guide you through choosing the Chatbot API, configuring the bot identity, and setting up the necessary OAuth and webhook infrastructure.
- “I am getting an invalid access token error when sending messages.”
- The agent will help you verify your app type, check if the required scopes are enabled, and confirm if the user has re-consented after recent scope changes.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/openai/build-zoom-team-chat-app/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip build-zoom-team-chat-app.zip -d .agents/skills
Documentation by Zoom
Build Zoom Team Chat App
Use this skill when the target surface is Zoom Team Chat. First decide whether the integration is user-scoped messaging, a chatbot, an interactive message-card workflow, or a webhook-driven automation.
Workflow
Choose the API surface: Team Chat API for user-scoped actions, Chatbot API for bot identity and chatbot workflows.
Confirm app type, scopes, role enablement, and whether the account has Zoom for Developers enabled.
Model message structure before coding: plain messages, rich cards, buttons, dropdowns, forms, slash commands, and threaded replies.
Implement OAuth and token refresh separately from message sending, with clear storage boundaries.
Add webhook handlers for interactivity and lifecycle events with signature verification and retry-safe processing.
Debug by checking JID formats, channel membership, bot installation, scopes, role settings, and message-card payload shape.
References
Full preserved guide: references/full-guide.md
API selection: concepts/api-selection.md
Message structure: concepts/message-structure.md
Message cards: references/message-cards.md
Scopes: references/scopes.md
Chatbot setup: examples/chatbot-setup.md
Common issues: troubleshooting/common-issues.md
Files
- ATTRIBUTION.mdAdded by AutoGineer880 B
- LICENSELicence file added by AutoGineer1.1 KiB
- RUNBOOK.md2.7 KiB
- SKILL.md1.5 KiB
- agents/openai.yaml145 B
- concepts/api-selection.md7.3 KiB
- concepts/authentication.md1.4 KiB
- concepts/deployment.md719 B
- concepts/environment-setup.md8.8 KiB
- concepts/message-structure.md675 B
- concepts/security.md478 B
- concepts/webhooks.md11.8 KiB
- examples/button-actions.md393 B
- examples/channel-management.md320 B
- examples/chatbot-setup.md12.2 KiB
- examples/database-integration.md312 B
- examples/dropdown-selects.md304 B
- examples/form-submissions.md385 B
- examples/llm-integration.md532 B
- examples/multi-step-workflows.md300 B
- examples/oauth-setup.md1.6 KiB
- examples/scheduled-alerts.md352 B
- examples/send-message.md417 B
- examples/slash-commands.md509 B
- examples/token-management.md442 B
- get-started.md1.6 KiB
- references/api-reference.md538 B
- references/environment-variables.md1006 B
- references/error-codes.md448 B
- references/full-guide.md22.9 KiB
- references/jid-formats.md247 B
- references/message-cards.md6.6 KiB
- references/rate-limits.md205 B
- references/sample-comparison.md1.1 KiB
- references/samples.md14.2 KiB
- references/scopes.md340 B
- references/webhook-events.md564 B
- troubleshooting/common-issues.md9.7 KiB
- troubleshooting/deployment-issues.md641 B
- troubleshooting/message-issues.md317 B
- troubleshooting/oauth-issues.md882 B
- troubleshooting/webhook-issues.md364 B
examples/chatbot-setup.md
Chatbot Setup - Complete Working Example
Build your first interactive Zoom chatbot from scratch. This guide provides complete, production-ready code.
Prerequisites
Completed Environment Setup
Obtained Bot JID, Client ID, Client Secret, Account ID, Secret Token
Created .env file with credentials
Project Structure
my-zoom-chatbot/
├── .env
├── .env.example
├── package.json
├── server.js
├── routes/
│ └── webhook.js
└── utils/
├── auth.js
├── chatbot.js
└── validation.jsStep 1: Initialize Project
mkdir my-zoom-chatbot
cd my-zoom-chatbot
npm init -yStep 2: Install Dependencies
npm install express dotenv node-fetchStep 3: Create .env File
# .env
ZOOM_CLIENT_ID=your_client_id_here
ZOOM_CLIENT_SECRET=your_client_secret_here
[email protected]
ZOOM_VERIFICATION_TOKEN=your_webhook_secret_token
ZOOM_ACCOUNT_ID=your_account_id
PORT=4000Step 4: Create Utility Files
utils/auth.js
// 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
// 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
// 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
};Step 5: Create Webhook Handler
routes/webhook.js
// 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
verifyZoomWebhookSignature(req);
const { event, payload } = req.body;
switch (event) {
case 'endpoint.url_validation':
return handleUrlValidation(req, res);
case 'bot_installed':
console.log('Bot installed for account:', payload.accountId);
return res.status(200).json({ success: true });
case 'bot_notification':
return handleBotNotification(payload, res);
case 'interactive_message_actions':
return handleButtonClick(payload, res);
case 'app_deauthorized':
console.log('Bot uninstalled for account:', payload.accountId);
return res.status(200).json({ success: true });
default:
console.log('Unsupported event:', event);
return 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 webhook signature' });
}
return res.status(500).json({ error: error.message });
}
}
/**
* Handle URL validation
*/
function handleUrlValidation(req, res) {
const { plainToken } = req.body.payload;
const encryptedToken = crypto
.createHmac('sha256', process.env.ZOOM_VERIFICATION_TOKEN)
.update(plainToken)
.digest('hex');
return res.status(200).json({
plainToken,
encryptedToken
});
}
/**
* Handle bot notification (slash command or direct message)
*/
async function handleBotNotification(payload, res) {
const { toJid, cmd, accountId, userName } = payload;
console.log(`${userName} sent: ${cmd}`);
// Respond immediately
res.status(200).json({ success: true });
// Process command asynchronously
try {
// Simple command router
if (cmd.toLowerCase().includes('help')) {
await sendTextMessage(toJid, accountId,
'Available commands:\n- help: Show this message\n- ping: Test bot\n- demo: Show demo buttons'
);
}
else if (cmd.toLowerCase().includes('ping')) {
await sendTextMessage(toJid, accountId, 'Pong! 🏓');
}
else if (cmd.toLowerCase().includes('demo')) {
await sendMessageWithButtons(toJid, accountId, {
title: 'Demo Buttons',
message: 'Click a button below:',
buttons: [
{ text: 'Option A', value: 'option_a', style: 'Primary' },
{ text: 'Option B', value: 'option_b', style: 'Default' },
{ text: 'Cancel', value: 'cancel', style: 'Danger' }
]
});
}
else {
await sendTextMessage(toJid, accountId,
`You said: "${cmd}"\n\nType "help" to see available commands.`
);
}
} catch (error) {
console.error('Error processing command:', error);
}
}
/**
* Handle button click
*/
async function handleButtonClick(payload, res) {
const { actionItem, toJid, accountId, userName } = payload;
console.log(`${userName} clicked: ${actionItem.value}`);
// Respond immediately
res.status(200).json({ success: true });
// Process button click asynchronously
try {
switch (actionItem.value) {
case 'option_a':
await sendTextMessage(toJid, accountId, '✅ You selected Option A');
break;
case 'option_b':
await sendTextMessage(toJid, accountId, '✅ You selected Option B');
break;
case 'cancel':
await sendTextMessage(toJid, accountId, '❌ Cancelled');
break;
default:
await sendTextMessage(toJid, accountId, `Unknown action: ${actionItem.value}`);
}
} catch (error) {
console.error('Error processing button click:', error);
}
}
module.exports = { handleWebhook };Step 6: Create Main Server
server.js
// 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`);
});Step 7: Test Locally with ngrok
# 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)Step 8: Configure Zoom Marketplace
Go to your app in Zoom Marketplace
Navigate to Features → Team Chat Subscription
Set Bot Endpoint URL:
https://abc123.ngrok.io/webhookSet Slash Command:
/mybotClick Save
Zoom will send a endpoint.url_validation request. If successful, you'll see a green checkmark.
Step 9: Install and Test
Go to Local Test page in Zoom Marketplace
Click Add App Now
Click Allow
Open Zoom Team Chat
In any channel, type:
/mybot help
You should see the bot respond with the help message!
Testing Checklist
[ ]
/mybot help- Shows help message[ ]
/mybot ping- Responds with "Pong! 🏓"[ ]
/mybot demo- Shows buttons[ ] Click button - Sends confirmation message
Production Deployment
Environment Variables
# Production .env
ZOOM_CLIENT_ID=your_production_client_id
ZOOM_CLIENT_SECRET=your_production_client_secret
[email protected] # Production Bot JID
ZOOM_VERIFICATION_TOKEN=your_production_token
ZOOM_ACCOUNT_ID=your_account_id
PORT=4000
NODE_ENV=productionDeploy to Cloud
Options:
Heroku
AWS Lambda
Google Cloud Run
Digital Ocean App Platform
Vercel (with serverless functions)
Requirements:
HTTPS endpoint (required for production)
Publicly accessible URL
Update Bot Endpoint URL in Zoom Marketplace to production URL
Next Steps
Button Actions - Advanced button handling
LLM Integration - Add an LLM provider
Message Cards Reference - Rich message components
Webhook Events Reference - All webhook events
Troubleshooting
| 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 |
Resources
Requirements and compatibility
Written for: OpenAI Codex
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
What our scanner noticed· Medium risk
- Possible data-exfiltration endpoints
- Instructions that use the network
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- Zoom
- Publisher
- OpenAI (Official)
- Distributed by
- OpenAI
- Source
- openai/plugins · plugins/zoom/skills/team-chat · commit d420d13
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from plugins/zoom/LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Zoom or OpenAI.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download build-zoom-team-chat-app.zip