
Build Zoom Team Chat App
- 1.4k installs
- 23.1k repo stars
- Updated July 28, 2026
- anthropics/knowledge-work-plugins
build-zoom-team-chat-app is an agent skill for reference skill for zoom team chat. use after routing to a chat workflow when building user-scoped messaging integrations, chatbot experiences, rich cards, buttons, slash.
About
The build-zoom-team-chat-app skill is designed for reference skill for Zoom Team Chat. Use after routing to a chat workflow when building user-scoped messaging integrations, chatbot experiences, rich cards, buttons, slash. /build-zoom-team-chat-app Background reference for Zoom Team Chat integrations. Use this after the workflow is clear, especially when the Team Chat API versus Chatbot API distinction matters. Invoke when the user asks about build zoom team chat app or related SKILL.md workflows.
- Chatbot Message Cards - Complete card component reference.
- Webhook Events - All webhook event types.
- API Reference - Endpoints, methods, parameters.
- Sample Applications - 10+ official sample apps.
- Integrated Index - see the section below in this file.
Build Zoom Team Chat App by the numbers
- 1,410 all-time installs (skills.sh)
- +81 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #261 of 1,896 Design & UI/UX skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
build-zoom-team-chat-app capabilities & compatibility
- Capabilities
- chatbot message cards complete card component · webhook events all webhook event types · api reference endpoints, methods, parameters · sample applications 10+ official sample apps
- Use cases
- frontend
What build-zoom-team-chat-app says it does
Reference skill for Zoom Team Chat. Use after routing to a chat workflow when building user-scoped messaging integrations, chatbot experiences, rich cards, buttons, slash commands,
Reference skill for Zoom Team Chat. Use after routing to a chat workflow when building user-scoped messaging integrations, chatbot experiences, rich cards, butt
npx skills add https://github.com/anthropics/knowledge-work-plugins --skill build-zoom-team-chat-appAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.4k |
|---|---|
| repo stars | ★ 23.1k |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 28, 2026 |
| Repository | anthropics/knowledge-work-plugins ↗ |
How do I reference skill for zoom team chat. use after routing to a chat workflow when building user-scoped messaging integrations, chatbot experiences, rich cards, buttons, slash?
Reference skill for Zoom Team Chat. Use after routing to a chat workflow when building user-scoped messaging integrations, chatbot experiences, rich cards, buttons, slash.
Who is it for?
Developers using build zoom team chat app workflows documented in SKILL.md.
Skip if: Skip when the task falls outside build-zoom-team-chat-app scope or needs a different stack.
When should I use this skill?
User asks about build zoom team chat app or related SKILL.md workflows.
What you get
Completed build-zoom-team-chat-app workflow with documented commands, files, and expected deliverables.
- API type decision
- Auth model selection
- Endpoint mapping
Files
/build-zoom-team-chat-app
Background reference for Zoom Team Chat integrations. Use this after the workflow is clear, especially when the Team Chat API versus Chatbot API distinction matters.
Read This First (Critical)
There are two different integration types and they are not interchangeable:
1. Team Chat API (user type)
- Sends messages as a real authenticated user
- Uses User OAuth (
authorization_code) - Endpoint family:
/v2/chat/users/...
2. Chatbot API (bot type)
- Sends messages as your bot identity
- Uses Client Credentials (
client_credentials) - Endpoint family:
/v2/im/chat/messages
If you choose the wrong type early, auth/scopes/endpoints all mismatch and implementation fails.
Official Documentation: https://developers.zoom.us/docs/team-chat/ Chatbot Documentation: https://developers.zoom.us/docs/team-chat/chatbot/extend/ API Reference: https://developers.zoom.us/docs/api/rest/reference/chatbot/
Quick Links
New to Team Chat? Follow this path:
1. [Get Started](get-started.md) - End-to-end fast path (user type vs bot type) 2. [Choose Your API](concepts/api-selection.md) - Team Chat API vs Chatbot API 3. [Environment Setup](concepts/environment-setup.md) - Credentials, scopes, app configuration 4. [OAuth Setup](examples/oauth-setup.md) - Complete authentication flow 5. [Send First Message](examples/send-message.md) - Working code to send messages
Reference:
- [Chatbot Message Cards](references/message-cards.md) - Complete card component reference
- [Webhook Events](references/webhook-events.md) - All webhook event types
- [API Reference](references/api-reference.md) - Endpoints, methods, parameters
- [Sample Applications](references/samples.md) - 10+ official sample apps
- Integrated Index - see the section below in this file
Having issues?
- Authentication errors → OAuth Troubleshooting
- Webhook not receiving events → Webhook Setup Guide
- Messages not sending → Common Issues
- Start with quick checks → 5-Minute Runbook
OAuth endpoint sanity check:
- Authorize URL:
https://zoom.us/oauth/authorize - Token URL:
https://zoom.us/oauth/token - If
/oauth/tokenreturns 404/HTML, usehttps://zoom.us/oauth/token.
Building Interactive Bots?
- Button Actions - Handle button clicks
- Form Submissions - Process form data
- Slash Commands - Create custom commands
Quick Decision: Which API?
| Use Case | API to Use |
|---|---|
| Send notifications from scripts/CI/CD | Team Chat API |
| Automate messages as a user | Team Chat API |
| Build an interactive chatbot | Chatbot API |
| Respond to slash commands | Chatbot API |
| Create messages with buttons/forms | Chatbot API |
| Handle user interactions | Chatbot API |
Team Chat API (User-Level)
- Messages appear as sent by authenticated user
- Requires User OAuth (authorization_code flow)
- Endpoint:
POST https://api.zoom.us/v2/chat/users/me/messages - Scopes:
chat_message:write,chat_channel:read
Chatbot API (Bot-Level)
- Messages appear as sent by your bot
- Requires Client Credentials grant
- Endpoint:
POST https://api.zoom.us/v2/im/chat/messages - Scopes:
imchat:bot(auto-added) - Rich cards: buttons, forms, dropdowns, images
Prerequisites
System Requirements
- Zoom account
- Account owner, admin, or Zoom for developers role enabled
- To enable: User Management → Roles → Role Settings → Advanced features → Enable Zoom for developers
Create Zoom App
1. Go to Zoom App Marketplace 2. Click Develop → Build App 3. Select General App (OAuth)
⚠️ Do NOT use Server-to-Server OAuth - S2S apps don't have the Chatbot/Team Chat feature. Only General App (OAuth) supports chatbots.
Required Credentials
From Zoom Marketplace → Your App:
| Credential | Location | Used By |
|---|---|---|
| Client ID | App Credentials → Development | Both APIs |
| Client Secret | App Credentials → Development | Both APIs |
| Account ID | App Credentials → Development | Chatbot API |
| Bot JID | Features → Chatbot → Bot Credentials | Chatbot API |
| Secret Token | Features → Team Chat Subscriptions | Chatbot API |
See: Environment Setup Guide for complete configuration steps.
Quick Start: Team Chat API
Send a message as a user:
// 1. Get access token via OAuth
const accessToken = await getOAuthToken(); // See examples/oauth-setup.md
// 2. Send message to channel
const response = await fetch('https://api.zoom.us/v2/chat/users/me/messages', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
message: 'Hello from CI/CD pipeline!',
to_channel: 'CHANNEL_ID'
})
});
const data = await response.json();
// { "id": "msg_abc123", "date_time": "2024-01-15T10:30:00Z" }Complete example: Send Message Guide
Quick Start: Chatbot API
Build an interactive chatbot:
// 1. Get chatbot token (client_credentials)
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}`,
'Content-Type': 'application/x-www-form-urlencoded'
},
body: 'grant_type=client_credentials'
});
return (await response.json()).access_token;
}
// 2. Send chatbot message with buttons
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({
robot_jid: process.env.ZOOM_BOT_JID,
to_jid: payload.toJid, // From webhook
account_id: payload.accountId, // From webhook
content: {
head: {
text: 'Build Notification',
sub_head: { text: 'CI/CD Pipeline' }
},
body: [
{ type: 'message', text: 'Deployment successful!' },
{
type: 'fields',
items: [
{ key: 'Branch', value: 'main' },
{ key: 'Commit', value: 'abc123' }
]
},
{
type: 'actions',
items: [
{ text: 'View Logs', value: 'view_logs', style: 'Primary' },
{ text: 'Dismiss', value: 'dismiss', style: 'Default' }
]
}
]
}
})
});Complete example: Chatbot Setup Guide
Key Features
Team Chat API
| Feature | Description |
|---|---|
| Send Messages | Post messages to channels or direct messages |
| List Channels | Get user's channels with metadata |
| Create Channels | Create public/private channels programmatically |
| Threaded Replies | Reply to specific messages in threads |
| Edit/Delete | Modify or remove messages |
Chatbot API
| Feature | Description |
|---|---|
| Rich Message Cards | Headers, images, fields, buttons, forms |
| Slash Commands | Custom /commands trigger webhooks |
| Button Actions | Interactive buttons with webhook callbacks |
| Form Submissions | Collect user input with forms |
| Dropdown Selects | Channel, member, date/time pickers |
| LLM Integration | Easy integration with Claude, GPT, etc. |
Webhook Events (Chatbot API)
| Event | Trigger | Use Case |
|---|---|---|
bot_notification | User messages bot or uses slash command | Process commands, integrate LLM |
bot_installed | Bot added to account | Initialize bot state |
interactive_message_actions | Button clicked | Handle button actions |
chat_message.submit | Form submitted | Process form data |
app_deauthorized | Bot removed | Cleanup |
See: Webhook Events Reference
Message Card Components
Build rich interactive messages with these components:
| Component | Description |
|---|---|
| header | Title and subtitle |
| message | Plain text |
| fields | Key-value pairs |
| actions | Buttons (Primary, Danger, Default styles) |
| section | Colored sidebar grouping |
| attachments | Images with links |
| divider | Horizontal line |
| form_field | Text input |
| dropdown | Select menu |
| date_picker | Date selection |
See: Message Cards Reference for complete component catalog
Architecture Patterns
Chatbot Lifecycle
User types /command → Webhook receives bot_notification
↓
payload.cmd = "user's input"
↓
Process command
↓
Send response via sendChatbotMessage()LLM Integration Pattern
case 'bot_notification': {
const { toJid, cmd, accountId } = payload;
// 1. Call your LLM
const llmResponse = await callClaude(cmd);
// 2. Send response back
await sendChatbotMessage(toJid, accountId, {
body: [{ type: 'message', text: llmResponse }]
});
}See: LLM Integration Guide
Sample Applications
| Sample | Description | Link |
|---|---|---|
| Chatbot Quickstart | Official tutorial (recommended start) | GitHub |
| Claude Chatbot | AI chatbot with Anthropic Claude | GitHub |
| Unsplash Chatbot | Image search with database | GitHub |
| ERP Chatbot | Oracle ERP with scheduled alerts | GitHub |
| Task Manager | Full CRUD app | GitHub |
See: Sample Applications Guide for analysis of all 10 samples
Common Operations
Send Message to Channel
// Team Chat API
await fetch('https://api.zoom.us/v2/chat/users/me/messages', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` },
body: JSON.stringify({
message: 'Hello!',
to_channel: 'CHANNEL_ID'
})
});Handle Button Click
// Webhook handler
case 'interactive_message_actions': {
const { actionItem, toJid, accountId } = payload;
if (actionItem.value === 'approve') {
await sendChatbotMessage(toJid, accountId, {
body: [{ type: 'message', text: '✅ Approved!' }]
});
}
}Verify Webhook Signature
function verifyWebhook(req) {
const message = `v0:${req.headers['x-zm-request-timestamp']}:${JSON.stringify(req.body)}`;
const hash = crypto.createHmac('sha256', process.env.ZOOM_VERIFICATION_TOKEN)
.update(message)
.digest('hex');
return req.headers['x-zm-signature'] === `v0=${hash}`;
}Deployment
ngrok for Local Development
# Install ngrok
npm install -g ngrok
# Expose local server
ngrok http 4000
# Use HTTPS URL as Bot Endpoint URL in Zoom Marketplace
# Example: https://abc123.ngrok.io/webhookProduction Deployment
See: Deployment Guide for:
- Nginx reverse proxy setup
- Base path configuration
- OAuth redirect URI setup
Limitations
| Limit | Value |
|---|---|
| Message length | 4,096 characters |
| File size | 512 MB |
| Members per channel | 10,000 |
| Channels per user | 500 |
Security Best Practices
1. Verify webhook signatures - Always validate using x-zm-signature header 2. Sanitize messages - Limit to 4096 chars, remove control characters 3. Validate JIDs - Check format: user@domain or channel@domain 4. Environment variables - Never hardcode credentials 5. Use HTTPS - Required for production webhooks
See: Security Best Practices
Complete Documentation Library
Core Concepts (Start Here!)
- [API Selection Guide](concepts/api-selection.md) - Choose Team Chat API vs Chatbot API
- [Environment Setup](concepts/environment-setup.md) - Complete credentials guide
- [Authentication Flows](concepts/authentication.md) - OAuth vs Client Credentials
- [Webhook Architecture](concepts/webhooks.md) - How webhooks work
- [Message Card Structure](concepts/message-structure.md) - Card component hierarchy
Complete Examples
- [OAuth Setup](examples/oauth-setup.md) - Full OAuth implementation
- [Send Message](examples/send-message.md) - Team Chat API message sending
- [Chatbot Setup](examples/chatbot-setup.md) - Complete chatbot with webhooks
- [Button Actions](examples/button-actions.md) - Handle interactive buttons
- [Form Submissions](examples/form-submissions.md) - Process form data
- [Slash Commands](examples/slash-commands.md) - Create custom commands
- [LLM Integration](examples/llm-integration.md) - Claude/GPT integration
- [Scheduled Alerts](examples/scheduled-alerts.md) - Cron + incoming webhooks
- [Channel Management](examples/channel-management.md) - Create/manage channels
References
- [API Reference](references/api-reference.md) - All endpoints and methods
- [Webhook Events](references/webhook-events.md) - Complete event reference
- [Message Cards](references/message-cards.md) - All card components
- [Sample Applications](references/samples.md) - Analysis of 10 official samples
- [Error Codes](references/error-codes.md) - Error handling guide
Troubleshooting
- [OAuth Issues](troubleshooting/oauth-issues.md) - Authentication failures
- [Webhook Issues](troubleshooting/webhook-issues.md) - Webhook debugging
- [Common Issues](troubleshooting/common-issues.md) - Quick diagnostics
Resources
- Official Docs: https://developers.zoom.us/docs/team-chat/
- API Reference: https://developers.zoom.us/docs/api/rest/reference/chatbot/
- Dev Forum: https://devforum.zoom.us/
- App Marketplace: https://marketplace.zoom.us/
---
Need help? Start with Integrated Index section below for complete navigation.
---
Integrated Index
_This section was migrated from SKILL.md._
Complete navigation guide for the Zoom Team Chat skill.
Quick Start Paths
- Start here: Get Started
- Fast troubleshooting first: 5-Minute Runbook
Path 1: Team Chat API (User-Level Messaging)
For sending messages as a user account.
1. API Selection Guide - Confirm Team Chat API is right 2. Environment Setup - Get credentials 3. OAuth Setup Example - Implement authentication 4. Send Message Example - Send your first message
Path 2: Chatbot API (Interactive Bots)
For building interactive chatbots with rich messages.
1. API Selection Guide - Confirm Chatbot API is right 2. Environment Setup - Get credentials (including Bot JID) 3. Webhook Architecture - Understand webhook events 4. Chatbot Setup Example - Build your first bot 5. Message Cards Reference - Create rich messages
Core Concepts
Essential understanding for both APIs.
| Document | Description |
|---|---|
| API Selection Guide | Choose Team Chat API vs Chatbot API |
| Environment Setup | Complete credentials and app configuration |
| Authentication Flows | OAuth vs Client Credentials |
| Webhook Architecture | How webhooks work (Chatbot API) |
| Message Card Structure | Card component hierarchy |
| Deployment Guide | Production deployment strategies |
| Security Best Practices | Secure your integration |
Complete Examples
Working code for common scenarios.
Authentication
| Example | Description |
|---|---|
| OAuth Setup | User OAuth flow implementation |
| Token Management | Refresh tokens, expiration handling |
Basic Operations
| Example | Description |
|---|---|
| Send Message | Team Chat API message sending |
| Chatbot Setup | Complete chatbot with webhooks |
| List Channels | Get user's channels |
| Create Channel | Create public/private channels |
Interactive Features (Chatbot API)
| Example | Description |
|---|---|
| Button Actions | Handle button clicks |
| Form Submissions | Process form data |
| Slash Commands | Create custom commands |
| Dropdown Selects | Channel/member pickers |
Advanced Integration
| Example | Description |
|---|---|
| LLM Integration | Integrate Claude/GPT |
| Scheduled Alerts | Cron + incoming webhooks |
| Database Integration | Store conversation state |
| Multi-Step Workflows | Complex user interactions |
References
API Documentation
| Reference | Description |
|---|---|
| API Reference | Pointers and common endpoints |
| Webhook Events | Event types and handling checklist |
| Message Cards | All card components |
| Error Codes | Error handling guide |
Sample Applications
| Reference | Description |
|---|---|
| Sample Applications | Sample app index/notes |
Field Guides
| Reference | Description |
|---|---|
| JID Formats | Understanding JID identifiers |
| Scopes Reference | Common scopes |
| Rate Limits | Throttling guidance |
Troubleshooting
| Guide | Description |
|---|---|
| Common Issues | Quick diagnostics and solutions |
| OAuth Issues | Authentication failures |
| Webhook Issues | Webhook debugging |
| Message Issues | Message sending problems |
| Deployment Issues | Production problems |
Architecture Patterns
Chatbot Lifecycle
User Action → Webhook → Process → ResponseLLM Integration Pattern
User Input → Chatbot receives → Call LLM → Send responseApproval Workflow Pattern
Request → Send card with buttons → User clicks → Update status → NotifyCommon Use Cases
Notifications
- CI/CD build notifications
- Server monitoring alerts
- Scheduled reports
- System health checks
Workflows
- Approval requests
- Task assignment
- Status updates
- Form submissions
Integrations
- LLM-powered assistants
- Database queries
- External API integration
- File/image sharing
Automation
- Scheduled messages
- Auto-responses
- Data collection
- Report generation
Resource Links
Official Documentation
- [Team Chat Docs](https://developers.zoom.us/docs/team-chat/) - Official overview
- [Chatbot Docs](https://developers.zoom.us/docs/team-chat/chatbot/extend/) - Chatbot guide
- [API Reference](https://developers.zoom.us/docs/api/rest/reference/chatbot/) - REST API docs
- [App Marketplace](https://marketplace.zoom.us/) - Create and manage apps
Sample Code
- [Chatbot Quickstart](https://github.com/zoom/chatbot-nodejs-quickstart) - Official tutorial
- [Claude Chatbot](https://github.com/zoom/zoom-chatbot-claude-sample) - AI integration
- [Unsplash Chatbot](https://github.com/zoom/unsplash-chatbot) - Image search bot
- [ERP Chatbot](https://github.com/zoom/zoom-erp-chatbot-sample) - Enterprise integration
- [Task Manager](https://github.com/zoom/task-manager-sample) - Full CRUD app
Tools
- [App Card Builder](https://appssdk.zoom.us/cardbuilder/) - Visual card designer
- [ngrok](https://ngrok.com/) - Local webhook testing
- [Postman](https://www.postman.com/) - API testing
Community
- [Developer Forum](https://devforum.zoom.us/) - Ask questions
- [GitHub Discussions](https://github.com/zoom) - Community support
- [Developer Support](https://devsupport.zoom.us) - Official support
Documentation Status
✅ Complete
- Main skill.md entry point
- API Selection Guide
- Environment Setup
- Webhook Architecture
- Chatbot Setup Example (complete working code)
- Message Cards Reference
- Common Issues Troubleshooting
📝 Pending (High Priority)
- OAuth Setup Example
- Send Message Example
- Button Actions Example
- LLM Integration Example
- Webhook Events Reference
- API Reference
- Sample Applications Analysis
📋 Planned (Lower Priority)
- Form Submissions Example
- Channel Management Examples
- Database Integration Example
- Error Codes Reference
- Rate Limits Guide
- Deployment troubleshooting
Getting Started Checklist
For Team Chat API
- [ ] Read API Selection Guide
- [ ] Complete Environment Setup
- [ ] Obtain Client ID, Client Secret
- [ ] Add required scopes
- [ ] Implement OAuth flow
- [ ] Send first message
For Chatbot API
- [ ] Read API Selection Guide
- [ ] Complete Environment Setup
- [ ] Obtain Client ID, Client Secret, Bot JID, Secret Token, Account ID
- [ ] Enable Team Chat in Features
- [ ] Configure Bot Endpoint URL and Slash Command
- [ ] Set up ngrok for local testing
- [ ] Implement webhook handler
- [ ] Send first chatbot message
Version History
- v1.0 (2026-02-09) - Initial comprehensive documentation
- Core concepts (API selection, environment setup, webhooks)
- Complete chatbot setup example
- Message cards reference
- Common issues troubleshooting
Support
Use this SKILL.md as the navigation hub for Team Chat API selection, setup, examples, and troubleshooting.
Environment Variables
- See references/environment-variables.md for standardized
.envkeys and where to find each value.
API Selection Guide
Zoom Team Chat offers two distinct APIs for different use cases. Choose the right one before you start building.
Critical First Decision
Pick one of these integration types before writing code:
- User type -> Team Chat API -> User OAuth ->
/v2/chat/users/... - Bot type -> Chatbot API -> Client Credentials ->
/v2/im/chat/messages
Most implementation issues come from mixing user-type auth with bot-type endpoints (or the opposite).
Quick Decision Matrix
| Use Case | API to Use | Messages Appear As |
|---|---|---|
| Send notifications from scripts/CI/CD | Team Chat API | Authenticated user |
| Automate messages as a user | Team Chat API | Authenticated user |
| Build an interactive chatbot | Chatbot API | Your bot |
| Respond to slash commands | Chatbot API | Your bot |
| Create messages with buttons/forms | Chatbot API | Your bot |
| Handle user interactions | Chatbot API | Your bot |
Team Chat API (User-Level Messaging)
What It Is
The Team Chat API allows your application to send messages as an authenticated user. Messages appear in Team Chat as if the user sent them manually.
When to Use
✅ Use Team Chat API when:
- You want to send simple text messages programmatically
- Messages should appear as sent by a specific user
- You're building CI/CD notifications
- You're automating user-level messaging
- You don't need interactive components (buttons, forms)
Key Characteristics
| Aspect | Details |
|---|---|
| Authentication | User OAuth (authorization_code flow) |
| Endpoint | POST https://api.zoom.us/v2/chat/users/me/messages |
| Message Format | Plain text or markdown |
| Scopes | chat_message:write, chat_channel:read |
| User Experience | Messages appear from the authenticated user |
Example Use Cases
1. CI/CD Notifications
User: "Build #123 completed successfully"2. Automated Reporting
User: "Daily sales report: $10,000"3. Task Reminders
User: "Reminder: Team meeting in 15 minutes"Chatbot API (Bot-Level Interactions)
What It Is
The Chatbot API allows your application to send messages as a bot. Bots can send rich, interactive messages with buttons, forms, images, and handle user interactions via webhooks.
When to Use
✅ Use Chatbot API when:
- You want to build an interactive chatbot
- You need rich message formatting (cards, buttons, forms)
- You want to handle slash commands (e.g.,
/weather) - You need to respond to button clicks or form submissions
- You're integrating LLMs (Claude, GPT, etc.)
- You want scheduled notifications
Key Characteristics
| Aspect | Details |
|---|---|
| Authentication | Client Credentials grant |
| Endpoint | POST https://api.zoom.us/v2/im/chat/messages |
| Message Format | Rich cards with components |
| Scopes | imchat:bot (auto-added) |
| User Experience | Messages appear from your bot |
| Interactivity | Buttons, forms, dropdowns, webhooks |
Example Use Cases
1. Support Bot
Bot: "How can I help you?"
[Help Center] [Contact Support] [Report Bug]2. Approval Workflow
Bot: "Expense Report: $500"
Branch: main
Requester: John
[Approve] [Reject]3. AI Assistant
User: "/ask What's the weather?"
Bot: "The weather in San Francisco is 72°F and sunny."Feature Comparison
| Feature | Team Chat API | Chatbot API |
|---|---|---|
| Plain Text Messages | ✅ | ✅ |
| Markdown | ✅ | ✅ |
| Rich Cards | ❌ | ✅ |
| Buttons | ❌ | ✅ |
| Forms | ❌ | ✅ |
| Dropdowns | ❌ | ✅ |
| Images | ✅ (basic) | ✅ (rich) |
| Slash Commands | ❌ | ✅ |
| Webhooks | ❌ | ✅ |
| Button Click Handling | ❌ | ✅ |
| Form Submissions | ❌ | ✅ |
Authentication Comparison
Team Chat API (User OAuth)
Flow: authorization_code Requires: User login and consent Token Scope: User's data only
// Step 1: Redirect user to OAuth consent page
const authUrl = `https://zoom.us/oauth/authorize?response_type=code&client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}`;
// Step 2: Exchange auth code for access token
const tokens = await exchangeCodeForToken(code);
// Step 3: Use access token to send messages
fetch('https://api.zoom.us/v2/chat/users/me/messages', {
headers: { 'Authorization': `Bearer ${tokens.access_token}` }
});Chatbot API (Client Credentials)
Flow: client_credentials Requires: No user login Token Scope: Bot actions only
// Step 1: Get bot token (no user interaction)
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'
});
const { access_token } = await response.json();
// Step 2: Use access token to send bot messages
fetch('https://api.zoom.us/v2/im/chat/messages', {
headers: { 'Authorization': `Bearer ${access_token}` }
});Can I Use Both?
Yes! You can use both APIs in the same application.
Example: Task management app
- Team Chat API: User creates a task → message appears as "User created task #123"
- Chatbot API: Bot sends reminders → "Task #123 is due today [View] [Snooze]"
Decision Tree
Need rich interactive messages?
├─ Yes → Chatbot API
└─ No
└─ Need webhooks (slash commands, button clicks)?
├─ Yes → Chatbot API
└─ No
└─ Messages should appear as user?
├─ Yes → Team Chat API
└─ No → Chatbot APICommon Misconceptions
❌ "I need to use Server-to-Server OAuth for bots"
Reality: Chatbots require General App (OAuth), not Server-to-Server OAuth. S2S apps don't support the Chatbot feature.
❌ "Team Chat API can send buttons"
Reality: Only Chatbot API supports interactive components (buttons, forms, dropdowns).
❌ "Chatbot API requires user login"
Reality: Chatbot API uses client_credentials flow (no user login needed).
❌ "OAuth token endpoint is /oauth/token"
Reality: Use https://zoom.us/oauth/token for token exchange. Keep https://zoom.us/oauth/authorize for the user consent step.
❌ "I can only use one API per app"
Reality: You can use both APIs in the same application.
Next Steps
If you chose Team Chat API:
1. Environment Setup - Get credentials 2. OAuth Setup - Implement OAuth flow 3. Send Message - Send your first message
If you chose Chatbot API:
1. Environment Setup - Get credentials (including Bot JID) 2. Chatbot Setup - Build your first bot 3. Webhook Architecture - Understand webhook events
Resources
Authentication Flows (Team Chat vs Chatbot)
Zoom Team Chat integrations commonly use one of two auth models:
Team Chat API (user-level)
Use User OAuth (authorization code) when you want messages/actions to appear as a user.
- Typical endpoints:
- Send message (as the user):
POST /v2/chat/users/me/messages - Typical scopes:
chat_message:writechat_channel:read(for listing channels)
Chatbot API (bot-level)
Use client credentials when you want messages/actions to appear as a bot.
- Typical endpoint:
- Send bot message:
POST /v2/im/chat/messages - Typical “scope”:
imchat:bot(added by enabling Chatbot feature on the app)
Decision Checklist
- If you need to post to a channel “as a bot” and handle slash command interactions: use Chatbot API.
- If you need to post “as the user” (and respect the user’s channel membership): use Team Chat API.
Common Pitfalls
- Server-to-Server OAuth is not a fit for Zoom Team Chat chatbot features.
- Team Chat API calls require a user token with the right scopes; “invalid access token” errors are almost always missing scopes or wrong app type.
- OAuth URL split is easy to mix up:
- authorize step:
https://zoom.us/oauth/authorize - token step (all grant types):
https://zoom.us/oauth/token - In browser demos, complete OAuth end-to-end in app (state verify -> callback -> code exchange -> token store) to avoid copy/paste mistakes.
Deployment Guide (Team Chat / Chatbot)
Basic Requirements
- Your webhook endpoint must be reachable by Zoom (public HTTPS).
- Keep secrets out of the repo:
ZOOM_CLIENT_IDZOOM_CLIENT_SECRETZOOM_BOT_JID(chatbot)ZOOM_SECRET_TOKEN(chatbot verification)
Recommended Production Setup
- Run behind a reverse proxy (TLS termination).
- Use a persistent store for:
- OAuth tokens (Team Chat API)
- installation state (Chatbot API)
- idempotency keys for webhooks (avoid double-processing)
Local Testing
- Use a tunneling tool to expose your local development host over HTTPS for webhook testing.
- Keep a "dev" app and "prod" app to avoid breaking production while iterating.
Environment Setup
Complete guide to configuring your Zoom Team Chat development environment, obtaining credentials, and setting up your app.
Prerequisites
- Zoom account
- Account owner, admin, or Zoom for developers role enabled
Enable "Zoom for developers" Role
If you don't have owner/admin privileges:
1. Ask your admin to enable the Zoom for developers role 2. Navigate to: User Management → Roles → Role Settings → Advanced features 3. Enable View and Edit checkboxes for Zoom for developers
Step 1: Create Zoom App
1.1 Access App Marketplace
1. Go to Zoom App Marketplace 2. Click Develop → Build App
1.2 Select App Type
Select: General App (OAuth)
⚠️ CRITICAL: Do NOT select "Server-to-Server OAuth"
>
Why: Server-to-Server OAuth apps do NOT support the Team Chat/Chatbot features. Only General App (OAuth) supports chatbots and team chat integrations.
Step 2: Basic Information
On the Basic Info page, configure your app:
2.1 App Name
Update the auto-generated app name:
- Click the edit icon (pencil)
- Enter your app name (e.g., "My Team Chat Bot")
- Click outside the field to save
2.2 App Management Type
Choose how your app is managed:
| Type | Use Case | Token Flow |
|---|---|---|
| Admin-managed | Company-wide bots, notifications, helpdesk | Recommended for chatbots |
| User-managed | Personal bots, individual user tools | For user-specific apps |
For most chatbots: Choose Admin-managed
Important: App management type affects available features and scopes. If you change it later, reconfirm your selected features and scopes.
2.3 App Credentials (Auto-generated)
The build flow automatically generates:
| Credential | Environment |
|---|---|
| Client ID | Development & Production |
| Client Secret | Development & Production |
Note: Development and production credentials are different.
2.4 OAuth Information
OAuth Redirect URL (Required)
Enter your OAuth callback endpoint:
Local development:
http://YOUR_DEV_HOST:4000/auth/callbackProduction:
https://yourdomain.com/auth/callbackOAuth Allow Lists (Required)
Add all URLs that Zoom should allow as valid OAuth redirects:
Examples:
- Complete URL:
https://subdomain.domain.tld/path/oauth/callback - Base URL:
https://subdomain.domain.tld
Step 3: Enable Team Chat (Chatbot API Only)
Skip this step if you're only using Team Chat API (user-level messaging)
3.1 Navigate to Features Page
Go to Features page → Surface tab
3.2 Select Team Chat Product
In Select where to use your app, check Team Chat
3.3 Configure App URLs
| Field | Value | Example |
|---|---|---|
| Home URL | Your app's home page | https://yourdomain.com |
| Domain Allow List | URLs Zoom client should accept | https://yourdomain.com |
3.4 Enable Team Chat Subscription
Configure webhook settings:
| Field | Value | Example |
|---|---|---|
| Slash Command | Command to invoke bot | /mybot |
| Bot Endpoint URL | Webhook endpoint | https://yourdomain.com/webhook |
Critical: Your bot will NOT appear in Team Chat unless you enable Team Chat Subscription!
Step 4: Get Credentials
4.1 App Credentials (Both APIs)
Navigate to App Credentials → Development:
| Credential | Where to Find |
|---|---|
| Client ID | App Credentials → Development |
| Client Secret | App Credentials → Development (Click "View") |
| Account ID | App Credentials → Development |
4.2 Bot JID (Chatbot API Only)
Note: Bot JID only appears AFTER enabling Chatbot in Features tab
To find Bot JID:
1. Go to Features tab in left sidebar 2. Ensure Chatbot toggle is ON 3. Click Chatbot section to expand 4. Scroll to Bot Credentials section 5. You'll see two JIDs:
- Bot JID (Development): Use for testing
- Bot JID (Production): Use for live apps
Format: v1abc123xyz@xmpp.zoom.us
4.3 Webhook Secret Token (Chatbot API Only)
Navigate to Features → Team Chat Subscriptions → Secret Token
This token is used to verify webhook signatures.
4.4 Credentials Summary
| Credential | Team Chat API | Chatbot API | Location |
|---|---|---|---|
| Client ID | ✅ Required | ✅ Required | App Credentials → Development |
| Client Secret | ✅ Required | ✅ Required | App Credentials → Development |
| Account ID | ❌ | ✅ Required | App Credentials → Development |
| Bot JID | ❌ | ✅ Required | Features → Chatbot → Bot Credentials |
| Secret Token | ❌ | ✅ Required | Features → Team Chat Subscriptions |
Step 5: Configure Scopes
Navigate to Scopes page in your app.
Team Chat API Scopes
Manually add these scopes:
chat_message:write- Send messageschat_message:read- Read messageschat_channel:read- List channelschat_channel:write- Create/manage channels
Chatbot API Scopes
When you enable Team Chat Subscription, these scopes are automatically added:
imchat:bot- Basic chatbot functionalityteam_chat:read:list_user_channels:admin- List channelsteam_chat:read:list_members:admin- List members
Step 6: Create .env File
For Team Chat API (User-Level)
# .env file
ZOOM_CLIENT_ID=your_client_id_here
ZOOM_CLIENT_SECRET=your_client_secret_here
ZOOM_REDIRECT_URI=http://YOUR_DEV_HOST:4000/auth/callback
PORT=4000For Chatbot API (Bot-Level)
# .env file
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.env.example Template
Create this file in your project root:
# Zoom App Credentials (Required for both APIs)
ZOOM_CLIENT_ID=
ZOOM_CLIENT_SECRET=
ZOOM_REDIRECT_URI=http://YOUR_DEV_HOST:4000/auth/callback
# Chatbot Credentials (Required for Chatbot API only)
ZOOM_BOT_JID=
ZOOM_VERIFICATION_TOKEN=
ZOOM_ACCOUNT_ID=
# Server Configuration
PORT=4000Step 7: Test Your App
On the Local Test page:
7.1 Add App to Your Account
1. Click Add App Now 2. Click Allow to authorize the app 3. You'll be redirected to your OAuth redirect URL
7.2 Preview App Listing
Click Preview Your App Listing Page to see how your app appears in the marketplace.
7.3 Share with Team Members
To share your app with other users on your account:
1. Go to Authorization URL section 2. Click Generate 3. Click Copy 4. Share the URL with your team members
Note: Beta apps can only be installed by members of the developer's Zoom account (security restriction).
Common Setup Issues
| Issue | Cause | Solution |
|---|---|---|
| Bot JID not visible | Chatbot feature not enabled | Go to Features tab, toggle Chatbot ON |
| Can't find Secret Token | Team Chat Subscription not enabled | Enable Team Chat Subscription in Features → Surface |
| OAuth redirect error | Redirect URL not in allow list | Add full redirect URL to OAuth allow lists |
| Scopes not appearing | Wrong app type | Verify you created General App (OAuth), not S2S |
| App can't be added | Missing required configuration | Complete all steps in Basic Info and Features |
Verification Checklist
Before proceeding to development, verify:
- [ ] Created General App (OAuth) (not Server-to-Server)
- [ ] Selected appropriate App Management Type
- [ ] Configured OAuth redirect URL
- [ ] Added URLs to OAuth allow lists
- [ ] Enabled Team Chat in Surface tab (for chatbots)
- [ ] Configured Team Chat Subscription (for chatbots)
- [ ] Added all required scopes
- [ ] Obtained all required credentials
- [ ] Created .env file with credentials
- [ ] Successfully added app to your account
Next Steps
For Team Chat API:
1. Authentication Flows - Understand OAuth 2. OAuth Setup Example - Implement OAuth 3. Send Message Example - Send first message
For Chatbot API:
1. Webhook Architecture - Understand webhooks 2. Chatbot Setup Example - Build your bot 3. Message Cards Reference - Create rich messages
Resources
Message Card Structure (Chatbot API)
Chatbot messages use a card-like JSON structure (often called "message cards").
High-Level Shape
content.head: title + optional subheadcontent.body: array of blocksmessageblocks for textfieldsblocks for key/value rowsactionsblocks for buttonsattachmentsblocks for images/links
Where To Look
- Component reference:
../references/message-cards.md
Common Pitfalls
- Buttons must include a
valueyou can route on when you receive an interaction webhook. - Many issues that look like "Zoom didn't render my card" are just invalid JSON shape; validate your payload before sending.
Security Best Practices
Webhooks
- Verify webhook requests using Zoom’s verification mechanism for Team Chat subscriptions.
- Treat webhook payloads as untrusted input; validate fields before using them.
OAuth
- Store refresh tokens securely (encrypt at rest).
- Rotate client secrets if they leak.
- Use least-privilege scopes.
Operational
- Add rate limiting on your webhook endpoint.
- Log request IDs and correlation IDs (but avoid logging tokens / PII).
Webhook Architecture
Complete guide to understanding and implementing Zoom Team Chat webhooks for interactive chatbots.
Overview
Webhooks are HTTP POST requests that Zoom sends to your Bot Endpoint URL when specific events occur (slash commands, button clicks, form submissions, etc.).
How It Works
User action in Zoom → Zoom sends webhook → Your server processes → Send responseExample flow:
1. User types "/weather San Francisco" in Zoom Team Chat
2. Zoom sends POST request to your Bot Endpoint URL
3. Your server receives webhook with payload.cmd = "San Francisco"
4. Your server calls weather API
5. Your server sends chatbot message back with weather dataWebhook Lifecycle
Setup (One-time)
1. Configure Bot Endpoint URL in Zoom Marketplace:
- Development:
https://abc123.ngrok.io/webhook - Production:
https://yourdomain.com/webhook
2. Verify endpoint - Zoom sends validation request when you save the URL
Runtime (Per Event)
User action → Zoom webhook → Your handler → ResponseWebhook Events
| Event | Trigger | When It Fires |
|---|---|---|
endpoint.url_validation | URL configured/changed | Setup only |
bot_installed | Bot added to account | Installation |
bot_notification | User messages bot or uses slash command | User interaction |
interactive_message_actions | Button clicked | User clicks button |
chat_message.submit | Form submitted | User submits form |
app_deauthorized | Bot removed from account | Uninstallation |
See: Webhook Events Reference for complete event catalog
Webhook Structure
Request Headers
Every webhook includes these headers:
{
'x-zm-signature': 'v0=abc123...', // Signature for verification
'x-zm-request-timestamp': '1234567890', // Unix timestamp
'content-type': 'application/json'
}Request Body
{
"event": "bot_notification", // Event type
"payload": { // Event-specific data
"accountId": "...",
"toJid": "...",
"cmd": "...",
// ... more fields
}
}Webhook Verification
CRITICAL: Always verify webhook signatures to prevent unauthorized requests.
Why Verify?
Without verification, anyone can send fake webhooks to your endpoint, potentially:
- Triggering unauthorized actions
- Causing denial-of-service attacks
- Accessing sensitive data
Verification Algorithm
const crypto = require('crypto');
function verifyZoomWebhookSignature(req) {
const signature = req.headers['x-zm-signature'];
const timestamp = req.headers['x-zm-request-timestamp'];
const secretToken = process.env.ZOOM_VERIFICATION_TOKEN;
if (!signature || !timestamp) {
throw new Error('Missing signature headers');
}
// Construct message
const message = `v0:${timestamp}:${JSON.stringify(req.body)}`;
// Calculate expected signature
const expectedSignature = crypto
.createHmac('sha256', secretToken)
.update(message)
.digest('hex');
// Compare signatures
if (signature !== `v0=${expectedSignature}`) {
throw new Error('Invalid webhook signature');
}
return true;
}Verification Flow
1. Extract signature and timestamp from headers
2. Construct message: "v0:{timestamp}:{JSON body}"
3. Calculate HMAC-SHA256 with secret token
4. Compare calculated signature with header signature
5. Accept if match, reject if mismatchWebhook Handler Pattern
Basic Handler
app.post('/webhook', (req, res) => {
try {
// Step 1: Verify signature
verifyZoomWebhookSignature(req);
// Step 2: Extract event and payload
const { event, payload } = req.body;
// Step 3: Handle event
switch (event) {
case 'endpoint.url_validation':
return handleUrlValidation(req, res);
case 'bot_installed':
return handleBotInstalled(payload, res);
case 'bot_notification':
return handleBotNotification(payload, res);
case 'interactive_message_actions':
return handleButtonClick(payload, res);
case 'app_deauthorized':
return handleBotUninstalled(payload, res);
default:
console.log('Unsupported event:', event);
return res.status(200).json({ success: true });
}
} catch (error) {
if (error.message.includes('signature')) {
return res.status(401).json({ error: 'Invalid webhook signature' });
}
return res.status(500).json({ error: error.message });
}
});Event Handlers
1. URL Validation (endpoint.url_validation)
Zoom sends this when you configure or change your Bot Endpoint URL.
Purpose: Verify you control the endpoint
Payload:
{
"event": "endpoint.url_validation",
"payload": {
"plainToken": "xyz123abc"
}
}Required Response:
{
"plainToken": "xyz123abc",
"encryptedToken": "hmac_sha256(plainToken, secret_token)"
}Implementation:
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
});
}2. Bot Installed (bot_installed)
Fired when someone adds your bot to their account.
Payload:
{
"event": "bot_installed",
"payload": {
"accountId": "...",
"userId": "...",
"timestamp": 1234567890
}
}Use Case: Initialize bot state, send welcome message
Implementation:
async function handleBotInstalled(payload, res) {
console.log('Bot installed for account:', payload.accountId);
// Optional: Initialize database, send welcome message
// await initializeBotForAccount(payload.accountId);
return res.status(200).json({ success: true });
}3. Bot Notification (bot_notification)
Fired when:
- User sends message to bot via slash command
- User sends direct message to bot
Payload:
{
"event": "bot_notification",
"payload": {
"accountId": "...",
"toJid": "channel@conference.xmpp.zoom.us",
"robotJid": "bot@xmpp.zoom.us",
"userJid": "user@xmpp.zoom.us",
"cmd": "user's input text",
"userName": "John Doe",
"channelName": "Marketing",
"timestamp": 1234567890
}
}Key Fields:
cmd- User's input after the slash commandtoJid- Where to send response (channel or DM)accountId- Account identifier
Use Case: Process commands, integrate LLM, send responses
Implementation:
async function handleBotNotification(payload, res) {
const { toJid, cmd, accountId, userName } = payload;
console.log(`${userName} sent: ${cmd}`);
// Process command (e.g., call LLM)
const response = await processCommand(cmd);
// Send response
await sendChatbotMessage(toJid, accountId, {
body: [{ type: 'message', text: response }]
});
return res.status(200).json({ success: true });
}4. Interactive Message Actions (interactive_message_actions)
Fired when user clicks a button in a chatbot message.
Payload:
{
"event": "interactive_message_actions",
"payload": {
"accountId": "...",
"toJid": "...",
"actionItem": {
"text": "Approve",
"value": "approve" // This is what you check
},
"messageId": "...",
"userName": "John Doe"
}
}Key Field: actionItem.value - The button's value you defined
Implementation:
async function handleButtonClick(payload, res) {
const { actionItem, toJid, accountId, userName } = payload;
console.log(`${userName} clicked: ${actionItem.value}`);
switch (actionItem.value) {
case 'approve':
await sendChatbotMessage(toJid, accountId, {
body: [{ type: 'message', text: '✅ Approved!' }]
});
break;
case 'reject':
await sendChatbotMessage(toJid, accountId, {
body: [{ type: 'message', text: '❌ Rejected' }]
});
break;
default:
console.log('Unknown action:', actionItem.value);
}
return res.status(200).json({ success: true });
}Webhook Best Practices
1. Always Verify Signatures
// ✅ GOOD
app.post('/webhook', (req, res) => {
verifyZoomWebhookSignature(req);
// ... handle event
});
// ❌ BAD
app.post('/webhook', (req, res) => {
// No verification - vulnerable to fake webhooks!
});2. Respond Quickly
Zoom expects a 200 response within 3 seconds.
// ✅ GOOD - Respond immediately, process async
app.post('/webhook', (req, res) => {
verifyZoomWebhookSignature(req);
// Respond immediately
res.status(200).json({ success: true });
// Process asynchronously
processWebhookAsync(req.body);
});
// ❌ BAD - Slow processing blocks response
app.post('/webhook', async (req, res) => {
await slowLLMCall(); // May timeout!
res.status(200).json({ success: true });
});3. Handle All Events Gracefully
// ✅ GOOD - Handle unknown events
switch (event) {
case 'bot_notification':
return handleBotNotification(payload, res);
default:
console.log('Unsupported event:', event);
return res.status(200).json({ success: true });
}
// ❌ BAD - Crash on unknown events
switch (event) {
case 'bot_notification':
return handleBotNotification(payload, res);
// Missing default case - crashes on new events!
}4. Log Webhook Activity
app.post('/webhook', (req, res) => {
const { event, payload } = req.body;
console.log(`[Webhook] ${event}`, {
timestamp: new Date().toISOString(),
accountId: payload.accountId,
userId: payload.userId
});
// ... handle event
});5. Use Environment Variables
// ✅ GOOD
const SECRET_TOKEN = process.env.ZOOM_VERIFICATION_TOKEN;
// ❌ BAD - Hardcoded secret
const SECRET_TOKEN = 'abc123xyz';Testing Webhooks
Local Development with ngrok
# Install ngrok
npm install -g ngrok
# Expose local server
ngrok http 4000
# Copy HTTPS URL to Zoom Marketplace
# Example: https://abc123.ngrok.io/webhookManual Testing
WEBHOOK_BASE_URL="http://YOUR_DEV_HOST:4000"
# Test with curl (will fail signature verification - expected)
curl -X POST "$WEBHOOK_BASE_URL/webhook" \
-H "Content-Type: application/json" \
-d '{"event":"test"}'
# Expected response: "Invalid webhook signature" (this is correct!)Verify Webhook is Working
Success indicators: 1. Zoom successfully validates your endpoint URL 2. bot_installed event fires when you add the bot 3. bot_notification fires when you use slash command 4. Button clicks trigger interactive_message_actions
Common Webhook Issues
| Issue | Cause | Solution |
|---|---|---|
| "Cannot GET /webhook" | Browser sends GET, webhook is POST | Normal - test with POST or Zoom |
| "Invalid signature" | Wrong secret token | Verify ZOOM_VERIFICATION_TOKEN matches Zoom Marketplace |
| URL validation fails | Response format incorrect | Return plainToken + encryptedToken |
| No webhooks received | Wrong endpoint URL | Verify URL in Zoom Marketplace matches your server |
| Webhooks timeout | Slow response | Return 200 immediately, process async |
Next Steps
- Webhook Events Reference - Complete event catalog
- Button Actions Example - Handle button clicks
- Slash Commands Example - Process slash commands
- LLM Integration Example - Integrate Claude/GPT
Resources
Button Actions (Chatbot API)
Buttons in message cards send a webhook when clicked.
Pattern
1. You send a card with actions.items[] where each button has a unique value. 2. Zoom sends interactive_message_actions to your webhook. 3. Your handler routes based on that value.
Routing Tip
Use stable action IDs like:
approve_requestreject_requestopen_ticket:123
Channel Management (Team Chat API)
Typical use cases:
- List channels a user can see (to let them pick a destination).
- Create channels (where supported by the API and account policy).
Pitfalls
- Many "can't list channels" issues are missing
chat_channel:read. - Admin policies may prevent channel creation.
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
ZOOM_BOT_JID=v1abc123xyz@xmpp.zoom.us
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
1. Go to your app in Zoom Marketplace 2. Navigate to Features → Team Chat Subscription 3. Set Bot Endpoint URL: https://abc123.ngrok.io/webhook 4. Set Slash Command: /mybot 5. Click Save
Zoom will send a endpoint.url_validation request. If successful, you'll see a green checkmark.
Step 9: Install and Test
1. Go to Local Test page in Zoom Marketplace 2. Click Add App Now 3. Click Allow 4. Open Zoom Team Chat 5. 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
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=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 Claude/GPT
- 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
Database Integration (Stateful Bots)
Store state when you need:
- multi-step workflows
- approvals
- linking Zoom users to internal system users
Suggested Tables
installations(account_id, bot_jid, created_at)users(zoom_jid, internal_user_id)workflows(workflow_id, status, payload_json)
Dropdown Selects (Chatbot API)
Dropdowns can be used for:
- picking a channel
- selecting a user
- selecting from a fixed list
Pattern
1. Send a card with a select/dropdown element. 2. User selects an option and submits (or triggers an action). 3. Handle selection via webhook and update state.
Form Submissions (Chatbot API)
Forms inside cards can collect user input; submissions arrive via webhook.
Pattern
1. Send a card with form fields. 2. Receive chat_message.submit webhook. 3. Validate inputs and respond with an updated card or confirmation message.
Pitfalls
- Always validate types (dates, numbers) server-side.
- Treat submitted text as untrusted input.
LLM Integration
Use an LLM to interpret user intent from Team Chat messages, then call Zoom APIs or respond with rich message cards.
Recommended flow:
1. Receive bot_notification event. 2. Extract user text and channel context. 3. Classify intent with your LLM (meeting actions, help, status). 4. Execute safe backend actions (for example, create/list meetings). 5. Send structured response back to Team Chat.
Implementation references:
- Chatbot Setup
- Sample Repositories
Multi-Step Workflows (Chatbot API)
Pattern
1. Send a card with buttons (step 1). 2. On click, update stored state and respond with step 2 card. 3. Repeat until completion.
Pitfalls
- Webhooks can be delivered more than once; de-dupe by event ID if available.
- Avoid storing PII in logs.
OAuth Setup (Team Chat API)
This is for the Team Chat API (user-level actions).
What You Need
- App type: General App (OAuth)
- Redirect URL: your app's callback URL
- Scopes (typical):
chat_message:writechat_channel:read
Flow Summary
1. Redirect user to Zoom authorize URL. 2. Receive code at your redirect URL. 3. Exchange code for access_token + refresh_token. 4. Store tokens per-user. 5. Refresh the access token when it expires.
In-App Web Flow Pattern (Recommended)
For browser demos, keep the whole flow in your app to avoid manual copy/paste mistakes:
1. User clicks Connect Zoom User in your UI. 2. Backend returns authorize URL (https://zoom.us/oauth/authorize) with a generated state. 3. Redirect browser to Zoom consent screen. 4. Callback route validates state and exchanges code at https://zoom.us/oauth/token. 5. Callback page stores token in app storage (for demo: localStorage, for production: server session/DB) and redirects back to app.
Token Exchange (Server Side)
Pseudo-code (Node style):
// POST https://zoom.us/oauth/token
// grant_type=authorization_code&code=...&redirect_uri=...
// Authorization: Basic base64(client_id:client_secret)Common Errors
Invalid redirect: redirect URL mismatch between code exchange and Marketplace config.Invalid access token, does not contain scopes: missing scopes on the app or user didn't re-consent after scope change.
Next
send-message.mdto post a message once you have a user token.token-management.mdfor refresh strategy.
Scheduled Alerts (Team Chat)
Two Common Approaches
1. Team Chat API (as user):
- Cron triggers, refresh user token, send message to a channel.
2. Chatbot API (as bot):
- Cron triggers, request bot token, send message card.
Pitfalls
- Don’t store tokens in plaintext.
- Ensure your cron job is idempotent (avoid duplicate messages).
Send Your First Message (Team Chat API)
This is for Team Chat API (messages sent as the authenticated user).
Endpoint
POST https://api.zoom.us/v2/chat/users/me/messages
Minimal Payload
{
"message": "Hello from my integration",
"to_channel": "CHANNEL_ID"
}Common Pitfalls
to_channelmust be a channel the user can access.- Use the correct scopes:
chat_message:write
Slash Commands (Chatbot API)
Slash commands are configured on the Marketplace app and trigger webhook events.
Pattern
1. Configure /yourcommand in the Chatbot feature settings. 2. User runs the command in Team Chat. 3. Your webhook receives bot_notification (or equivalent) with the command text. 4. Parse args and respond with a message card.
Pitfalls
- Commands are account-scoped; make sure you're testing in the right account.
- Don’t rely on client-side parsing; parse on your server.
Token Management (Team Chat API)
Storage
Store per-user:
access_tokenrefresh_tokenexpires_at(absolute timestamp)
Refresh Strategy
- Refresh "just-in-time" when an API call fails with token expiry, or
- Refresh proactively when
now >= expires_at - 60s.
Pitfalls
- Refresh tokens can expire or be revoked (user removes app, admin blocks app).
- When you change scopes, existing users may need to reauthorize.
Team Chat Get Started
This is the fast path for Zoom Team Chat integrations.
Step 1: Pick Integration Type First
- User type (Team Chat API)
- Auth:
authorization_code(User OAuth) - Endpoint family:
/v2/chat/users/... - Messages appear as user
- Bot type (Chatbot API)
- Auth:
client_credentials - Endpoint family:
/v2/im/chat/messages - Messages appear as bot
If this decision is wrong, auth/scopes/endpoints will all mismatch.
Step 2: Set Up App + Credentials
1. Create General App (OAuth) in Zoom Marketplace. 2. Configure scopes and feature settings. 3. Gather credentials from app config:
ZOOM_CLIENT_IDZOOM_CLIENT_SECRETZOOM_BOT_JID(bot type)ZOOM_ACCOUNT_ID(bot type use cases)
See: concepts/environment-setup.md
Step 3A: User Type (Team Chat API)
1. Implement OAuth code flow. 2. Call POST /v2/chat/users/me/messages with bearer token. 3. Use OAuth endpoints correctly:
- authorize:
https://zoom.us/oauth/authorize - token exchange:
https://zoom.us/oauth/token
See:
examples/oauth-setup.mdexamples/send-message.md
Step 3B: Bot Type (Chatbot API)
1. Get token via grant_type=client_credentials. 2. Call POST /v2/im/chat/messages. 3. Add webhook endpoint for interactive events. 4. Use https://zoom.us/oauth/token for client_credentials token requests.
See:
examples/chatbot-setup.mdconcepts/webhooks.mdreferences/message-cards.md
Step 4: Validate with a Minimal Smoke Test
- User type: send one plain text channel message.
- Bot type: send one plain text bot message.
Then add advanced features (buttons/forms/slash commands).
API Reference Pointers
This doc is intentionally lightweight; prefer the official REST reference for the authoritative schema.
Team Chat API (user-level)
- Send message:
POST /v2/chat/users/me/messages - Typical needs:
- list channels
- post to channel / DM
- thread replies
Chatbot API (bot-level)
- Send bot message:
POST /v2/im/chat/messages
Notes
- If you see "invalid access token" errors, check:
- app type (General App OAuth vs others)
- scopes
- whether the user re-consented after scope changes
Zoom Team Chat Environment Variables
Standard .env keys
| Variable | Required | Used for | Where to find |
|---|---|---|---|
ZOOM_CLIENT_ID | Yes | Team Chat app OAuth identity | Zoom Marketplace -> Team Chat app -> App Credentials |
ZOOM_CLIENT_SECRET | Yes | OAuth token exchange | Zoom Marketplace -> Team Chat app -> App Credentials |
ZOOM_REDIRECT_URI | OAuth code flow | Callback URL for installs/auth | Zoom Marketplace -> OAuth redirect/allow list |
ZOOM_BOT_JID | Chatbot flows | Target bot identifier | Team Chat app/chatbot configuration after setup |
ZOOM_SECRET_TOKEN | Recommended | Event/webhook signature verification | Zoom Marketplace -> Event Subscriptions -> Secret Token |
ZOOM_VERIFICATION_TOKEN | Legacy only | Legacy verification path | Zoom Marketplace legacy fields (older apps) |
Runtime-only values
ZOOM_ACCESS_TOKENZOOM_REFRESH_TOKEN
Notes
- Prefer secret-token signature verification over legacy verification token.
Error Codes (Common Patterns)
Auth Errors
Invalid access token- wrong token type (bot token used for user API, or vice versa)
- missing scopes
- token expired / revoked
Webhook Errors
- No events received:
- endpoint not reachable publicly
- verification failing
- wrong event subscription / wrong app/account
Message Rendering Issues
- Card not rendering:
- invalid JSON payload
- unsupported component types
JID Formats (Quick Guide)
JIDs identify users/bots in Team Chat contexts.
Practical Tips
- Treat JIDs as opaque identifiers.
- Store them exactly as received.
- Don’t parse structure unless Zoom explicitly documents the format you need.
Message Card Components Reference
Complete reference for building rich interactive messages in Zoom Team Chat chatbots.
Card Structure
Every chatbot message has this structure:
{
"content": {
"head": { // Optional header
"text": "Title",
"sub_head": { "text": "Subtitle" }
},
"body": [ // Array of components
{ "type": "message", "text": "Content" },
{ "type": "actions", "items": [...] }
// ... more components
]
}
}Components Catalog
Text Components
message
Plain text content.
{
"type": "message",
"text": "Hello, this is plain text"
}header
Title text with optional styling.
{
"type": "header",
"text": "Main Heading",
"style": {
"bold": true,
"italic": false
}
}styled_text
Text with markdown-like styling.
{
"type": "styled_text",
"text": "**Bold** *italic* `code`"
}Interactive Components
actions (Buttons)
Clickable buttons that trigger webhooks.
{
"type": "actions",
"items": [
{
"text": "Approve",
"value": "approve",
"style": "Primary" // Primary, Danger, Default
},
{
"text": "Reject",
"value": "reject",
"style": "Danger"
}
]
}Styles:
Primary- Blue buttonDanger- Red buttonDefault- Gray button
dropdown
Select menu with options.
{
"type": "dropdown",
"select_items": [
{ "text": "Option 1", "value": "opt1" },
{ "text": "Option 2", "value": "opt2" }
]
}form_field
Text input field.
{
"type": "form_field",
"editable": true,
"text": "Enter your name"
}Layout Components
section
Group components with optional colored sidebar.
{
"type": "section",
"sidebar_color": "#3b82f6", // Hex color
"sections": [
{ "type": "message", "text": "Grouped content" }
]
}Common colors:
- Success:
#10b981(green) - Error:
#ef4444(red) - Warning:
#f59e0b(orange) - Info:
#3b82f6(blue)
fields
Key-value pairs displayed in columns.
{
"type": "fields",
"items": [
{ "key": "Status", "value": "Active" },
{ "key": "Priority", "value": "High" },
{ "key": "Assignee", "value": "John Doe" }
]
}divider
Horizontal line separator.
{
"type": "divider"
}Media Components
attachments
Image with optional link.
{
"type": "attachments",
"img_url": "https://example.com/image.jpg",
"resource_url": "https://example.com/full-page",
"information": {
"title": { "text": "Image Title" },
"description": { "text": "Click to view" }
}
}Complete Examples
Build Notification
{
"content": {
"head": {
"text": "Build #123 Complete",
"sub_head": { "text": "main branch" }
},
"body": [
{
"type": "section",
"sidebar_color": "#10b981",
"sections": [
{ "type": "message", "text": "✅ Build completed successfully" }
]
},
{
"type": "fields",
"items": [
{ "key": "Branch", "value": "main" },
{ "key": "Commit", "value": "abc123" },
{ "key": "Duration", "value": "2m 34s" }
]
},
{
"type": "actions",
"items": [
{ "text": "View Logs", "value": "view_logs", "style": "Primary" },
{ "text": "Deploy", "value": "deploy", "style": "Default" }
]
}
]
}
}Approval Request
{
"content": {
"head": {
"text": "Expense Approval Required"
},
"body": [
{ "type": "message", "text": "John Doe submitted an expense report" },
{
"type": "fields",
"items": [
{ "key": "Amount", "value": "$500.00" },
{ "key": "Category", "value": "Travel" },
{ "key": "Date", "value": "Feb 9, 2026" }
]
},
{ "type": "divider" },
{
"type": "actions",
"items": [
{ "text": "Approve", "value": "approve_500", "style": "Primary" },
{ "text": "Reject", "value": "reject_500", "style": "Danger" },
{ "text": "View Details", "value": "details_500", "style": "Default" }
]
}
]
}
}Error Notification
{
"content": {
"head": {
"text": "⚠️ Service Alert"
},
"body": [
{
"type": "section",
"sidebar_color": "#ef4444",
"sections": [
{ "type": "message", "text": "Database connection failed" }
]
},
{
"type": "fields",
"items": [
{ "key": "Service", "value": "api-prod" },
{ "key": "Error", "value": "Connection timeout" },
{ "key": "Time", "value": "2026-02-09 18:30:00 UTC" }
]
},
{
"type": "actions",
"items": [
{ "text": "View Logs", "value": "logs", "style": "Primary" },
{ "text": "Acknowledge", "value": "ack", "style": "Default" }
]
}
]
}
}Limitations
| Component | Limit |
|---|---|
| Message text | 4,096 characters |
| Button text | 40 characters |
| Field key/value | 256 characters each |
| Dropdown options | 100 options |
| Buttons per message | 5 buttons |
Best Practices
Button Design
✅ DO: Use clear, action-oriented labels
- "Approve Request"
- "View Details"
- "Cancel Order"
❌ DON'T: Use vague labels
- "OK"
- "Click Here"
- "Button"
Color Usage
✅ DO: Use semantic colors
- Green (
#10b981) for success - Red (
#ef4444) for errors/destructive actions - Blue (
#3b82f6) for info - Orange (
#f59e0b) for warnings
❌ DON'T: Use random colors without meaning
Field Formatting
✅ DO: Keep keys concise, values informative
{ "key": "Status", "value": "Active" }❌ DON'T: Make keys too long
{ "key": "The current status of the request", "value": "Active" }Testing Cards
Use the Team Chat App Card Builder to:
- Preview card designs
- Test layouts
- Generate JSON
Next Steps
- Chatbot Setup - Build your first bot
- Button Actions - Handle button clicks
- Webhook Events - Understand webhook payloads
Resources
Rate Limits
Rate limits vary by endpoint and account. If you get throttled:
- add retries with exponential backoff
- batch work where possible
- avoid calling list endpoints repeatedly (cache results)
Sample Comparison
Use this quick matrix to choose the right Team Chat or chatbot sample shape before building your own integration.
| Sample shape | Best when | Strengths | Watch-outs |
|---|---|---|---|
| Minimal webhook bot | You want to validate event flow quickly | Fastest setup, easy signature verification review | Usually in-memory state only |
| Full chatbot sample | You need slash commands, cards, and bot responses together | Shows end-to-end chat lifecycle | More moving parts than a simple receiver |
| LLM-enhanced bot | You want summarization or assistant behavior | Good reference for prompt assembly and response shaping | Requires stricter latency and fallback design |
| Multi-language sample | You need parity across runtimes | Useful for comparing auth and verification patterns | Feature coverage often drifts between languages |
What to Compare
- OAuth flow type and token handling
- Webhook signature verification approach
- Message card rendering support
- Persistence model: in-memory, file, or database
- Local development strategy: tunnel, mock events, replay fixtures
Sample Applications Analysis
Analysis of 10 official Zoom Team Chat sample applications, extracted patterns, and best practices.
Sample Overview
| Sample | Language | Complexity | Best For |
|---|---|---|---|
| chatbot-nodejs-quickstart | Node.js | ⭐ Beginner | Start here - Tutorial series |
| zoom-chatbot-claude-sample | Node.js | ⭐⭐ Intermediate | LLM integration pattern |
| unsplash-chatbot | Node.js | ⭐⭐ Intermediate | API integration + database |
| zoom-erp-chatbot-sample | Node.js | ⭐⭐⭐ Advanced | Enterprise integration |
| task-manager-sample | Node.js | ⭐⭐⭐ Advanced | Full CRUD application |
| zoom-cohere-chatbot-sample | Node.js | ⭐⭐ Intermediate | Cohere LLM integration |
| zoom-cerebras-chatbot-sample | Node.js | ⭐⭐ Intermediate | Cerebras LLM integration |
| zoom-team-chat-shortcut-sample | Node.js | ⭐⭐ Intermediate | Shortcuts and UI elements |
| zoom-teams-chat-snowflake-sample | Node.js | ⭐⭐⭐ Advanced | Snowflake data integration |
| rivet-javascript-sample | Node.js | ⭐⭐ Intermediate | Rivet SDK usage |
1. chatbot-nodejs-quickstart
Repository: https://github.com/zoom/chatbot-nodejs-quickstart
Description: Official tutorial series covering 9 episodes from setup to advanced features.
Key Features:
- Setup & Send Messages
- Handle Events
- Slash Commands
- Markdown & Emojis
- Reactions & Interactive Messages
- Threaded Replies
- Search Messages via API
- Scheduling Messages
- Zoom Workplace App Integration
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:
Webhook Handler Pattern
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
}
}Token Generation
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:
- ✅ Signature verification on all webhooks
- ✅ Environment variables for credentials
- ✅ Modular route structure
- ✅ Error handling with try/catch
- ✅ Immediate webhook response (200 status)
Recommended For: First-time chatbot developers
2. zoom-chatbot-claude-sample
Repository: https://github.com/zoom/zoom-chatbot-claude-sample
Description: AI-powered chatbot using Anthropic Claude for natural language responses.
Key Features:
- Claude API integration
- Conversation history tracking
- Streaming responses (optional)
- Context management
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
3. unsplash-chatbot
Repository: https://github.com/zoom/unsplash-chatbot
Description: Image search bot integrating Unsplash API with database storage.
Key Features:
- Third-party API integration (Unsplash)
- Database persistence (SQLite/PostgreSQL)
- Image search and display
- User preference storage
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:
- ✅ API rate limiting handling
- ✅ Error handling for external APIs
- ✅ Database connection pooling
- ✅ User data privacy
Recommended For: External API integration patterns
4. zoom-erp-chatbot-sample
Repository: https://github.com/zoom/zoom-erp-chatbot-sample
Description: Enterprise Resource Planning integration with scheduled alerts.
Key Features:
- Oracle ERP API integration
- Scheduled notifications (cron)
- Approval workflows
- Threaded conversations
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
5. task-manager-sample
Repository: https://github.com/zoom/task-manager-sample
Description: Full-featured task management application with CRUD operations.
Key Features:
- Create, read, update, delete tasks
- Task assignment
- Due date tracking
- Status management
- Persistent storage
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
Common Patterns Across Samples
1. Environment Variable Management
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=development2. Project Structure
Common 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.json3. Webhook Verification
All 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}`;
}4. Error Handling
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' });
}
});5. Async Webhook Processing
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);
});
});Architecture Lessons
Chatbot Lifecycle
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 ZoomState Management
Simple 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]);
}
};Deprecation Notes
Some samples may use deprecated patterns:
❌ Old Pattern (Don't Use)
// Hardcoded credentials
const CLIENT_ID = 'abc123';✅ New Pattern (Use This)
// Environment variables
const CLIENT_ID = process.env.ZOOM_CLIENT_ID;❌ Old Pattern (Don't Use)
// Synchronous webhook processing (may timeout)
app.post('/webhook', async (req, res) => {
await longRunningProcess();
res.status(200).json({ success: true });
});✅ New Pattern (Use This)
// Async processing
app.post('/webhook', (req, res) => {
res.status(200).json({ success: true });
longRunningProcess().catch(console.error);
});Sample Selection Guide
Choose chatbot-nodejs-quickstart if:
- You're new to Zoom chatbots
- You want a tutorial series
- You need step-by-step guidance
Choose zoom-chatbot-claude-sample if:
- You want to integrate an LLM
- You need conversational AI
- You want to see LLM integration patterns
Choose unsplash-chatbot if:
- You need to integrate external APIs
- You want database patterns
- You need user preference storage
Choose zoom-erp-chatbot-sample if:
- You're building enterprise integrations
- You need scheduled notifications
- You want approval workflows
Choose task-manager-sample if:
- You want a full CRUD application
- You need complex state management
- You want to see production architecture
Next Steps
- Chatbot Setup Example - Build your own using these patterns
- LLM Integration Example - Integrate Claude/GPT
- Button Actions Example - Handle interactive components
- Sample Comparison - Compare common sample shapes before choosing a baseline
Resources
Scopes Reference (Common)
Team Chat API
Common scopes include:
chat_message:writechat_channel:read
Chatbot API
The Chatbot feature uses bot credentials; typical setup includes enabling the feature and using the bot token.
Pitfall
After adding scopes in Marketplace, users often need to reauthorize to grant them.
Webhook Events (Chatbot API)
Common webhook event types you will handle:
bot_notification: user messages your bot or triggers a commandinteractive_message_actions: user clicks a buttonchat_message.submit: user submits a formbot_installed: bot added to an accountapp_deauthorized: bot removed / app deauthorized
Handler Checklist
- Verify the request (per Zoom's verification guidance).
- Parse payload carefully (treat as untrusted input).
- Route by event type and action values.
- Respond quickly; do heavy work async if needed.
Team Chat 5-Minute Preflight Runbook
Use this before deep debugging. It catches the most common Team Chat failures fast.
Skill Doc Standard Note
- Agent-skill standard entrypoint is
SKILL.md. - This runbook is an operational convention (recommended), not a required skill file.
SKILL.mdis also a navigation convention for larger skill docs.
1) Confirm Integration Type
- User type (Team Chat API): user OAuth +
/v2/chat/users/... - Bot type (Chatbot API): client credentials +
/v2/im/chat/messages
If this is wrong, everything else will fail.
2) Confirm OAuth Endpoints
- Authorize URL:
https://zoom.us/oauth/authorize - Token URL:
https://zoom.us/oauth/token
If token requests hit /oauth/token, expect 404/HTML.
3) Confirm Runtime Env Loading
If credentials are split by mode, verify your server loads the actual files at runtime:
project/team-chat-api/.envproject/chatbot-api/.env
Do not assume root .env is enough.
4) Confirm App Routes + Reverse Proxy
- Current demo pages:
/team-chat/user-demo/team-chat/bot-demo- API path should resolve:
/team-chat/api/*
If browser calls old routes (/api/channel/*) and gets 404, either update frontend or keep compatibility routes.
5) Run Curl Probes
Use backend probes before browser debugging.
TEAM_CHAT_BASE_URL="http://YOUR_HOST:YOUR_PORT"
curl -sS "$TEAM_CHAT_BASE_URL/team-chat/api/config"
curl -sS -i "$TEAM_CHAT_BASE_URL/team-chat/api/bot/token"
curl -sS -i "$TEAM_CHAT_BASE_URL/team-chat/api/channel/list"Expected:
api/configshows required flags as configured.api/bot/tokenshould return JSON (200 or actionable 4xx), never HTML 404 page.api/channel/listreturns validation errors or data, not generic 404.
6) Browser-Specific Reality Check
ERR_BLOCKED_BY_CLIENT usually means extension/adblock/privacy filter interference.
- Re-test in Incognito.
- Temporarily disable blockers for host.
- Validate with curl first.
7) User OAuth Callback Flow (In-App)
For user-demo, avoid manual copy/paste flow:
1. UI button triggers backend authorize URL generation with state. 2. Browser redirects to Zoom consent page. 3. Callback validates state and exchanges code server-side. 4. Token is stored where UI expects (session/db/local storage for demo). 5. Redirect back to user-demo.
If callback returns but token is missing, focus on state validation and persistence path.
8) Fast Decision Tree
- 404 on bot token -> check token URL (
/oauth/token), then proxy path. - All channel APIs 404 -> route mismatch (old UI vs new backend routes).
- OAuth works but sends fail -> wrong scopes or app type mismatch.
- Works by curl but fails in browser -> blocked client/cached old JS.
Common Issues and Solutions
Quick diagnostics and solutions for Zoom Team Chat development.
Authentication Issues
"Invalid client_id or client_secret"
Cause: Incorrect credentials or using wrong environment (dev vs production)
Solution: 1. Verify credentials in .env match Zoom Marketplace 2. Check you're using Development credentials (not Production) 3. Regenerate Client Secret if needed
"Get Bot Token" returns 404 or HTML page
Cause: Using wrong token endpoint.
Fix:
- Use
https://zoom.us/oauth/tokenfor token exchange. - Do not use
https://zoom.us/oauth/tokenfor chatbot token requests.
Quick check:
curl -X POST https://zoom.us/oauth/token \
-H "Authorization: Basic <base64(client_id:client_secret)>" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials""Token expired"
Cause: Access token has expired (1 hour for user tokens)
Solution:
// Implement token refresh
if (error.message.includes('token expired')) {
const newToken = await refreshAccessToken(refreshToken);
// Retry request with new token
}"Scope not authorized"
Cause: Missing required scope in app configuration
Solution: 1. Go to Zoom Marketplace → Your App → Scopes 2. Add missing scope (e.g., chat_message:write) 3. Users must re-authorize the app
Webhook Issues
"Cannot GET /webhook" (Browser)
Expected Behavior: This is NORMAL
Explanation: Webhooks are POST-only. Browsers send GET requests.
Test properly:
WEBHOOK_BASE_URL="http://YOUR_DEV_HOST:4000"
# Use POST instead
curl -X POST "$WEBHOOK_BASE_URL/webhook" \
-H "Content-Type: application/json" \
-d '{"event":"test"}'"Invalid webhook signature"
Cause: Mismatch between your Secret Token and Zoom's
Solution: 1. Verify ZOOM_VERIFICATION_TOKEN in .env 2. Check Secret Token in Zoom Marketplace → Features → Team Chat Subscriptions 3. Ensure no extra spaces/characters in token
Debug:
console.log('Expected token:', process.env.ZOOM_VERIFICATION_TOKEN);
console.log('Signature from Zoom:', req.headers['x-zm-signature']);URL Validation Fails
Cause: Incorrect response format
Correct response:
{
"plainToken": "xyz123",
"encryptedToken": "hmac_sha256_hash"
}Incorrect:
{ "success": true } // Wrong!No Webhooks Received
Checklist:
- [ ] ngrok is running:
ngrok http 4000 - [ ] Bot Endpoint URL in Zoom Marketplace matches ngrok URL
- [ ] Server is running:
node server.js - [ ] Slash command configured in Zoom Marketplace
- [ ] Bot installed in your account
Test:
# In Zoom Team Chat, type:
/yourbot test
# Should see webhook in server logsBot JID Issues
"Bot JID not found"
Cause: Chatbot feature not enabled
Solution: 1. Go to Zoom Marketplace → Your App → Features 2. Toggle Chatbot ON 3. Bot JID will appear in Bot Credentials section
"Bot JID appears but messages not sending"
Cause: Wrong Bot JID format or environment mismatch
Solution: 1. Verify format: v1abc123xyz@xmpp.zoom.us 2. Use Development Bot JID for testing 3. Check Account ID matches the bot's account
Message Sending Issues
"Messages not appearing in Team Chat"
Common causes:
1. Wrong `to_jid`
// Use toJid from webhook payload
await sendMessage(payload.toJid, accountId, content);2. Missing `account_id`
// Required for chatbot messages
{
"account_id": process.env.ZOOM_ACCOUNT_ID, // Don't forget!
"robot_jid": process.env.ZOOM_BOT_JID,
"to_jid": toJid
}3. Incorrect content format
// ❌ Wrong
{ "text": "Hello" }
// ✅ Correct
{
"content": {
"body": [
{ "type": "message", "text": "Hello" }
]
}
}"Message truncated or garbled"
Cause: Special characters or exceeding 4096 char limit
Solution:
function sanitizeMessage(message) {
return message
.trim()
.replace(/[\x00-\x1F\x7F]/g, '') // Remove control chars
.substring(0, 4096); // Enforce limit
}Button/Form Issues
"Buttons not clickable"
Cause: Missing value field
Incorrect:
{
"type": "actions",
"items": [
{ "text": "Click Me" } // Missing value!
]
}Correct:
{
"type": "actions",
"items": [
{ "text": "Click Me", "value": "clicked" }
]
}"Button clicks not triggering webhooks"
Checklist:
- [ ] Webhook handler has
interactive_message_actionscase - [ ] Bot Endpoint URL configured correctly
- [ ] Server responding with 200 status
- [ ] Webhook signature verification passing
ngrok Issues
"ngrok session expired"
Cause: Free ngrok URLs expire after 2 hours
Solutions: 1. Short-term: Restart ngrok, update Bot Endpoint URL 2. Long-term: Use ngrok paid plan or deploy to production
"ngrok URL changes every restart"
Free plan behavior: URL changes each time
Solutions: 1. Use ngrok auth token for persistent URLs (paid) 2. Use environment variable for flexibility:
const WEBHOOK_URL = process.env.WEBHOOK_URL || 'https://YOUR_PUBLIC_WEBHOOK_URL/webhook';Deployment Issues
"Works locally but not in production"
Common causes:
1. Environment variables not set
# Verify all vars exist
echo $ZOOM_CLIENT_ID
echo $ZOOM_CLIENT_SECRET
echo $ZOOM_BOT_JID2. HTTP instead of HTTPS
- Production MUST use HTTPS
- Zoom rejects HTTP endpoints
3. Port binding issues
// Use PORT from environment
const PORT = process.env.PORT || 4000;4. Credentials exist, but wrong `.env` file is loaded
- If your app keeps per-mode env files (for example
project/team-chat-api/.envandproject/chatbot-api/.env), make sure runtime loads those files explicitly. - Verify loaded config via a health/config endpoint before debugging OAuth logic.
404 on /team-chat/api/channel/*
Cause: Route mismatch between old and new demo structure.
Fix:
- New pages should use:
/team-chat/user-demo/team-chat/bot-demo- Keep compatibility routes in backend if older UI still calls:
/api/channel/list/api/channel/messages/api/channel/message
Browser shows ERR_BLOCKED_BY_CLIENT
Cause: Browser extension/adblock/privacy filter blocked a request.
What to do:
- Test in Incognito or with extensions disabled for your host.
- Confirm backend route with
curlbefore treating this as server failure.
Rate Limiting
"Rate limit exceeded"
Zoom Limits:
- 10 requests/second per user
- 100 requests/second per app
Solution:
// Implement exponential backoff
async function retryWithBackoff(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (error.status === 429) {
const delay = Math.pow(2, i) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
} else {
throw error;
}
}
}
throw new Error('Max retries exceeded');
}General App Issues
"App not appearing in Team Chat"
Cause: Team Chat surface not enabled
Solution: 1. Go to Zoom Marketplace → Your App → Features → Surface 2. Check Team Chat 3. Configure Home URL and Domain Allow List 4. Save changes
"Users can't install the app"
Cause: App not in Local Test or not published
Solutions: 1. For testing: Go to Local Test → Generate Authorization URL → Share with team 2. For production: Submit app for Zoom review and publish
Debugging Tools
Log All Webhooks
app.post('/webhook', (req, res) => {
console.log('=== Webhook Received ===');
console.log('Event:', req.body.event);
console.log('Payload:', JSON.stringify(req.body.payload, null, 2));
console.log('Headers:', req.headers);
// ... handle webhook
});Test Token Generation
// Test script: test-token.js
require('dotenv').config();
const { getChatbotToken } = require('./utils/auth');
(async () => {
try {
const token = await getChatbotToken();
console.log('✅ Token generated successfully');
console.log('Token:', token.substring(0, 20) + '...');
} catch (error) {
console.error('❌ Token error:', error.message);
}
})();Verify Credentials
// verify-setup.js
require('dotenv').config();
const required = [
'ZOOM_CLIENT_ID',
'ZOOM_CLIENT_SECRET',
'ZOOM_BOT_JID',
'ZOOM_VERIFICATION_TOKEN',
'ZOOM_ACCOUNT_ID'
];
console.log('=== Credential Check ===');
required.forEach(key => {
const value = process.env[key];
if (!value) {
console.error(`❌ Missing: ${key}`);
} else {
console.log(`✅ ${key}: ${value.substring(0, 10)}...`);
}
});Getting Help
Before Asking for Help
1. Check error messages in console/logs 2. Verify all credentials are correct 3. Test with curl or Postman 4. Review official samples
Where to Get Help
Include in Support Requests
1. Zoom app type (General App OAuth) 2. Error message (full text) 3. Code snippet (sanitized - no credentials!) 4. Steps to reproduce 5. Expected vs actual behavior
Next Steps
- Webhook Architecture - Deep dive into webhooks
- Chatbot Setup - Complete working example
- API Reference - Endpoint documentation
Deployment Issues
Works Locally, Fails in Prod
- DNS/HTTPS misconfiguration
- blocked outbound calls from your environment
- missing env vars / secrets
- wrong env file loaded at runtime (for split setups like
team-chat-api/.envandchatbot-api/.env)
Quick Prod Checklist
- Confirm token endpoint is
https://zoom.us/oauth/token - Confirm user OAuth authorize URL is
https://zoom.us/oauth/authorize - Confirm current UI routes are
/team-chat/user-demoand/team-chat/bot-demo - Confirm reverse proxy forwards
/team-chat/api/*correctly
Webhooks Time Out
- Respond fast and move long-running work to async jobs.
Message Issues
Messages Not Sending
- Confirm you're using the correct API:
- Team Chat API uses user OAuth token
- Chatbot API uses bot token +
robot_jid
Card Not Rendering
- Validate the card JSON payload against known-good examples.
- Simplify to a minimal card and add components incrementally.
OAuth Issues (Team Chat API)
"Invalid redirect" / redirect mismatch
- The redirect URL in the token exchange must exactly match what's configured in Marketplace.
- Keep endpoint split correct:
- authorize:
https://zoom.us/oauth/authorize - token exchange:
https://zoom.us/oauth/token
"Invalid access token, does not contain scopes"
- Add the scope in Marketplace.
- Ensure the user re-authorizes after scope changes.
- Confirm you're using the user token for Team Chat API calls.
Token Expired
- Refresh access tokens using the refresh token.
- If refresh fails, the user likely needs to reauthorize.
Callback succeeds but app still has no token
- Verify callback route actually exchanges
codeserver-side. - Verify
stateis validated and not expired. - Verify token is persisted where your UI expects it (session/database/local storage for demo).
Webhook Issues (Chatbot API)
No Events Arriving
- Ensure your endpoint is publicly reachable over HTTPS.
- Confirm the correct app/account is installed and subscribed.
- Check verification settings (secret token, validation flow).
Duplicate Events
- Webhooks can be delivered more than once.
- Add idempotency (store processed event IDs if available).
Related skills
FAQ
What does build-zoom-team-chat-app do?
Reference skill for Zoom Team Chat. Use after routing to a chat workflow when building user-scoped messaging integrations, chatbot experiences, rich cards, buttons, slash.
When should I use build-zoom-team-chat-app?
User asks about build zoom team chat app or related SKILL.md workflows.
Is build-zoom-team-chat-app safe to install?
Review the Security Audits panel on this page before installing in production.