concepts/api-selection.md
# 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
```javascript
// 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
```javascript
// 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 API
```
## Common 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](environment-setup.md) - Get credentials
2. [OAuth Setup](../examples/oauth-setup.md) - Implement OAuth flow
3. [Send Message](../examples/send-message.md) - Send your first message
### If you chose **Chatbot API**:
1. [Environment Setup](environment-setup.md) - Get credentials (including Bot JID)
2. [Chatbot Setup](../examples/chatbot-setup.md) - Build your first bot
3. [Webhook Architecture](webhooks.md) - Understand webhook events
## Resources
- [Official Team Chat API Docs](https://developers.zoom.us/docs/api/rest/reference/chat/)
- [Official Chatbot API Docs](https://developers.zoom.us/docs/api/rest/reference/chatbot/)
- [Chatbot Quickstart Sample](https://github.com/zoom/chatbot-nodejs-quickstart)
concepts/authentication.md
# 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:write`
- `chat_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.
concepts/deployment.md
# 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_ID`
- `ZOOM_CLIENT_SECRET`
- `ZOOM_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.
concepts/environment-setup.md
# 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](https://marketplace.zoom.us/)
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/callback
```
**Production**:
```
https://yourdomain.com/auth/callback
```
#### OAuth 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 messages
- `chat_message:read` - Read messages
- `chat_channel:read` - List channels
- `chat_channel:write` - Create/manage channels
### Chatbot API Scopes
When you enable Team Chat Subscription, these scopes are **automatically added**:
- `imchat:bot` - Basic chatbot functionality
- `team_chat:read:list_user_channels:admin` - List channels
- `team_chat:read:list_members:admin` - List members
## Step 6: Create .env File
### For Team Chat API (User-Level)
```bash
# .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=4000
```
### For Chatbot API (Bot-Level)
```bash
# .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:
```bash
# 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=4000
```
## Step 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](authentication.md) - Understand OAuth
2. [OAuth Setup Example](../examples/oauth-setup.md) - Implement OAuth
3. [Send Message Example](../examples/send-message.md) - Send first message
### For Chatbot API:
1. [Webhook Architecture](webhooks.md) - Understand webhooks
2. [Chatbot Setup Example](../examples/chatbot-setup.md) - Build your bot
3. [Message Cards Reference](../references/message-cards.md) - Create rich messages
## Resources
- [Zoom App Marketplace](https://marketplace.zoom.us/)
- [OAuth Documentation](https://developers.zoom.us/docs/integrations/oauth/)
- [Chatbot Documentation](https://developers.zoom.us/docs/team-chat/chatbot/extend/)
- [Using Role Management](https://support.zoom.us/hc/en-us/articles/115001078646)
concepts/message-structure.md
# Message Card Structure (Chatbot API)
Chatbot messages use a card-like JSON structure (often called "message cards").
## High-Level Shape
- `content.head`: title + optional subhead
- `content.body`: array of blocks
- `message` blocks for text
- `fields` blocks for key/value rows
- `actions` blocks for buttons
- `attachments` blocks for images/links
## Where To Look
- Component reference: `../references/message-cards.md`
## Common Pitfalls
- Buttons must include a `value` you 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.
concepts/security.md
# 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).
concepts/webhooks.md
# 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 response
```
**Example 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 data
```
## Webhook 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 → Response
```
## Webhook 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](../references/webhook-events.md) for complete event catalog
## Webhook Structure
### Request Headers
Every webhook includes these headers:
```javascript
{
'x-zm-signature': 'v0=abc123...', // Signature for verification
'x-zm-request-timestamp': '1234567890', // Unix timestamp
'content-type': 'application/json'
}
```
### Request Body
```javascript
{
"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
```javascript
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 mismatch
```
## Webhook Handler Pattern
### Basic Handler
```javascript
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**:
```javascript
{
"event": "endpoint.url_validation",
"payload": {
"plainToken": "xyz123abc"
}
}
```
**Required Response**:
```javascript
{
"plainToken": "xyz123abc",
"encryptedToken": "hmac_sha256(plainToken, secret_token)"
}
```
**Implementation**:
```javascript
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**:
```javascript
{
"event": "bot_installed",
"payload": {
"accountId": "...",
"userId": "...",
"timestamp": 1234567890
}
}
```
**Use Case**: Initialize bot state, send welcome message
**Implementation**:
```javascript
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**:
```javascript
{
"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 command
- `toJid` - Where to send response (channel or DM)
- `accountId` - Account identifier
**Use Case**: Process commands, integrate LLM, send responses
**Implementation**:
```javascript
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**:
```javascript
{
"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**:
```javascript
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
```javascript
// ✅ 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.
```javascript
// ✅ 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
```javascript
// ✅ 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
```javascript
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
```javascript
// ✅ GOOD
const SECRET_TOKEN = process.env.ZOOM_VERIFICATION_TOKEN;
// ❌ BAD - Hardcoded secret
const SECRET_TOKEN = 'abc123xyz';
```
## Testing Webhooks
### Local Development with ngrok
```bash
# Install ngrok
npm install -g ngrok
# Expose local server
ngrok http 4000
# Copy HTTPS URL to Zoom Marketplace
# Example: https://abc123.ngrok.io/webhook
```
### Manual Testing
```bash
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](../references/webhook-events.md) - Complete event catalog
- [Button Actions Example](../examples/button-actions.md) - Handle button clicks
- [Slash Commands Example](../examples/slash-commands.md) - Process slash commands
- [LLM Integration Example](../examples/llm-integration.md) - Integrate Claude/GPT
## Resources
- [Chatbot Webhook Events](https://developers.zoom.us/docs/api/chatbot/events/)
- [Webhook Verification](https://developers.zoom.us/docs/api/webhooks/#verify-webhook-events)
- [Chatbot Quickstart](https://github.com/zoom/chatbot-nodejs-quickstart)
examples/button-actions.md
# 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_request`
- `reject_request`
- `open_ticket:123`
examples/channel-management.md
# 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.
examples/chatbot-setup.md
# Chatbot Setup - Complete Working Example
Build your first interactive Zoom chatbot from scratch. This guide provides complete, production-ready code.
## Prerequisites
- Completed [Environment Setup](../concepts/environment-setup.md)
- 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.js
```
## Step 1: Initialize Project
```bash
mkdir my-zoom-chatbot
cd my-zoom-chatbot
npm init -y
```
## Step 2: Install Dependencies
```bash
npm install express dotenv node-fetch
```
## Step 3: Create .env File
```bash
# .env
ZOOM_CLIENT_ID=your_client_id_here
ZOOM_CLIENT_SECRET=your_client_secret_here
ZOOM_BOT_JID=v1abc123xyz@xmpp.zoom.us
ZOOM_VERIFICATION_TOKEN=your_webhook_secret_token
ZOOM_ACCOUNT_ID=your_account_id
PORT=4000
```
## Step 4: Create Utility Files
### utils/auth.js
```javascript
// 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
```javascript
// 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
```javascript
// 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
```javascript
// 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
```javascript
// 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
```bash
# 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](https://marketplace.zoom.us/)
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
```bash
# 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=production
```
### Deploy 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](button-actions.md) - Advanced button handling
- [LLM Integration](llm-integration.md) - Add Claude/GPT
- [Message Cards Reference](../references/message-cards.md) - Rich message components
- [Webhook Events Reference](../references/webhook-events.md) - 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
- [Chatbot Quickstart (Official)](https://github.com/zoom/chatbot-nodejs-quickstart)
- [Claude Chatbot Sample](https://github.com/zoom/zoom-chatbot-claude-sample)
- [Unsplash Chatbot Sample](https://github.com/zoom/unsplash-chatbot)
examples/database-integration.md
# 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)
examples/dropdown-selects.md
# 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.
examples/form-submissions.md
# 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.
examples/llm-integration.md
# 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](chatbot-setup.md)
- [Sample Repositories](../references/samples.md)
examples/multi-step-workflows.md
# 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.
examples/oauth-setup.md
# 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:write`
- `chat_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):
```js
// 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.md` to post a message once you have a user token.
- `token-management.md` for refresh strategy.
examples/scheduled-alerts.md
# 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).
examples/send-message.md
# 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
```json
{
"message": "Hello from my integration",
"to_channel": "CHANNEL_ID"
}
```
## Common Pitfalls
- `to_channel` must be a channel the user can access.
- Use the correct scopes:
- `chat_message:write`
examples/slash-commands.md
# 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.
examples/token-management.md
# Token Management (Team Chat API)
## Storage
Store per-user:
- `access_token`
- `refresh_token`
- `expires_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.
get-started.md
# 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_ID`
- `ZOOM_CLIENT_SECRET`
- `ZOOM_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.md`
- `examples/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.md`
- `concepts/webhooks.md`
- `references/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).
references/api-reference.md
# 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
references/environment-variables.md
# 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_TOKEN`
- `ZOOM_REFRESH_TOKEN`
## Notes
- Prefer secret-token signature verification over legacy verification token.
references/error-codes.md
# 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
references/jid-formats.md
# 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.
references/message-cards.md
# Message Card Components Reference
Complete reference for building rich interactive messages in Zoom Team Chat chatbots.
## Card Structure
Every chatbot message has this structure:
```javascript
{
"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.
```javascript
{
"type": "message",
"text": "Hello, this is plain text"
}
```
#### header
Title text with optional styling.
```javascript
{
"type": "header",
"text": "Main Heading",
"style": {
"bold": true,
"italic": false
}
}
```
#### styled_text
Text with markdown-like styling.
```javascript
{
"type": "styled_text",
"text": "**Bold** *italic* `code`"
}
```
### Interactive Components
#### actions (Buttons)
Clickable buttons that trigger webhooks.
```javascript
{
"type": "actions",
"items": [
{
"text": "Approve",
"value": "approve",
"style": "Primary" // Primary, Danger, Default
},
{
"text": "Reject",
"value": "reject",
"style": "Danger"
}
]
}
```
**Styles**:
- `Primary` - Blue button
- `Danger` - Red button
- `Default` - Gray button
#### dropdown
Select menu with options.
```javascript
{
"type": "dropdown",
"select_items": [
{ "text": "Option 1", "value": "opt1" },
{ "text": "Option 2", "value": "opt2" }
]
}
```
#### form_field
Text input field.
```javascript
{
"type": "form_field",
"editable": true,
"text": "Enter your name"
}
```
### Layout Components
#### section
Group components with optional colored sidebar.
```javascript
{
"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.
```javascript
{
"type": "fields",
"items": [
{ "key": "Status", "value": "Active" },
{ "key": "Priority", "value": "High" },
{ "key": "Assignee", "value": "John Doe" }
]
}
```
#### divider
Horizontal line separator.
```javascript
{
"type": "divider"
}
```
### Media Components
#### attachments
Image with optional link.
```javascript
{
"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
```javascript
{
"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
```javascript
{
"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
```javascript
{
"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
```javascript
{ "key": "Status", "value": "Active" }
```
❌ **DON'T**: Make keys too long
```javascript
{ "key": "The current status of the request", "value": "Active" }
```
## Testing Cards
Use the [Team Chat App Card Builder](https://appssdk.zoom.us/cardbuilder/) to:
- Preview card designs
- Test layouts
- Generate JSON
## Next Steps
- [Chatbot Setup](../examples/chatbot-setup.md) - Build your first bot
- [Button Actions](../examples/button-actions.md) - Handle button clicks
- [Webhook Events](webhook-events.md) - Understand webhook payloads
## Resources
- [Official Card Components](https://developers.zoom.us/docs/team-chat/customizing-messages/)
- [App Card Builder](https://appssdk.zoom.us/cardbuilder/)
- [Sample Chatbots](https://github.com/zoom?q=chatbot)
references/rate-limits.md
# 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)
references/sample-comparison.md
# 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
references/samples.md
# 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](https://github.com/zoom/chatbot-nodejs-quickstart) | Node.js | ⭐ Beginner | **Start here** - Tutorial series |
| [zoom-chatbot-claude-sample](https://github.com/zoom/zoom-chatbot-claude-sample) | Node.js | ⭐⭐ Intermediate | LLM integration pattern |
| [unsplash-chatbot](https://github.com/zoom/unsplash-chatbot) | Node.js | ⭐⭐ Intermediate | API integration + database |
| [zoom-erp-chatbot-sample](https://github.com/zoom/zoom-erp-chatbot-sample) | Node.js | ⭐⭐⭐ Advanced | Enterprise integration |
| [task-manager-sample](https://github.com/zoom/task-manager-sample) | Node.js | ⭐⭐⭐ Advanced | Full CRUD application |
| [zoom-cohere-chatbot-sample](https://github.com/zoom/zoom-cohere-chatbot-sample) | Node.js | ⭐⭐ Intermediate | Cohere LLM integration |
| [zoom-cerebras-chatbot-sample](https://github.com/zoom/zoom-cerebras-chatbot-sample) | Node.js | ⭐⭐ Intermediate | Cerebras LLM integration |
| [zoom-team-chat-shortcut-sample](https://github.com/zoom/zoom-team-chat-shortcut-sample) | Node.js | ⭐⭐ Intermediate | Shortcuts and UI elements |
| [zoom-teams-chat-snowflake-sample](https://github.com/zoom/zoom-teams-chat-snowflake-sample) | Node.js | ⭐⭐⭐ Advanced | Snowflake data integration |
| [rivet-javascript-sample](https://github.com/zoom/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 variables
```
**Key Patterns**:
### Webhook Handler Pattern
```javascript
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
```javascript
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**:
```javascript
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**:
```javascript
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**:
```bash
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**:
```sql
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**:
```javascript
{
"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**:
```javascript
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**:
```javascript
// 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**:
```javascript
// 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:
```bash
# 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=development
```
### 2. 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.json
```
### 3. Webhook Verification
All samples verify webhook signatures:
```javascript
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:
```javascript
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:
```javascript
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 Zoom
```
### State Management
**Simple bots**: In-memory state (Map/Object)
**Production bots**: Database (PostgreSQL, MongoDB, Redis)
```javascript
// 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)
```javascript
// Hardcoded credentials
const CLIENT_ID = 'abc123';
```
### ✅ New Pattern (Use This)
```javascript
// Environment variables
const CLIENT_ID = process.env.ZOOM_CLIENT_ID;
```
### ❌ Old Pattern (Don't Use)
```javascript
// Synchronous webhook processing (may timeout)
app.post('/webhook', async (req, res) => {
await longRunningProcess();
res.status(200).json({ success: true });
});
```
### ✅ New Pattern (Use This)
```javascript
// 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](../examples/chatbot-setup.md) - Build your own using these patterns
- [LLM Integration Example](../examples/llm-integration.md) - Integrate Claude/GPT
- [Button Actions Example](../examples/button-actions.md) - Handle interactive components
- [Sample Comparison](sample-comparison.md) - Compare common sample shapes before choosing a baseline
## Resources
- [Official Samples GitHub Org](https://github.com/zoom?q=chatbot)
- [Chatbot Documentation](https://developers.zoom.us/docs/team-chat/chatbot/extend/)
- [Developer Forum](https://devforum.zoom.us/)
references/scopes.md
# Scopes Reference (Common)
## Team Chat API
Common scopes include:
- `chat_message:write`
- `chat_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.
references/webhook-events.md
# Webhook Events (Chatbot API)
Common webhook event types you will handle:
- `bot_notification`: user messages your bot or triggers a command
- `interactive_message_actions`: user clicks a button
- `chat_message.submit`: user submits a form
- `bot_installed`: bot added to an account
- `app_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.
RUNBOOK.md
# 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.md` is 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/.env`
- `project/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.
```bash
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/config` shows required flags as configured.
- `api/bot/token` should return JSON (200 or actionable 4xx), never HTML 404 page.
- `api/channel/list` returns 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.
SKILL.md
---
name: build-zoom-team-chat-app
description: "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, or chat webhooks."
triggers:
- "zoom team chat"
- "zoom chatbot"
- "zoom messaging"
- "team chat api"
- "chatbot api"
- "zoom slash commands"
- "zoom chat integration"
---
# /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](troubleshooting/oauth-issues.md)
- Webhook not receiving events → [Webhook Setup Guide](troubleshooting/webhook-issues.md)
- Messages not sending → [Common Issues](troubleshooting/common-issues.md)
- Start with quick checks → [5-Minute Runbook](RUNBOOK.md)
**OAuth endpoint sanity check:**
- Authorize URL: `https://zoom.us/oauth/authorize`
- Token URL: `https://zoom.us/oauth/token`
- If `/oauth/token` returns 404/HTML, use `https://zoom.us/oauth/token`.
**Building Interactive Bots?**
- [Button Actions](examples/button-actions.md) - Handle button clicks
- [Form Submissions](examples/form-submissions.md) - Process form data
- [Slash Commands](examples/slash-commands.md) - 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](https://marketplace.zoom.us/)
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](concepts/environment-setup.md) for complete configuration steps.
## Quick Start: Team Chat API
Send a message as a user:
```javascript
// 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](examples/send-message.md)
## Quick Start: Chatbot API
Build an interactive chatbot:
```javascript
// 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](examples/chatbot-setup.md)
## 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](references/webhook-events.md)
## 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](references/message-cards.md) 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
```javascript
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](examples/llm-integration.md)
## Sample Applications
| Sample | Description | Link |
|--------|-------------|------|
| **Chatbot Quickstart** | Official tutorial (recommended start) | [GitHub](https://github.com/zoom/chatbot-nodejs-quickstart) |
| **Claude Chatbot** | AI chatbot with Anthropic Claude | [GitHub](https://github.com/zoom/zoom-chatbot-claude-sample) |
| **Unsplash Chatbot** | Image search with database | [GitHub](https://github.com/zoom/unsplash-chatbot) |
| **ERP Chatbot** | Oracle ERP with scheduled alerts | [GitHub](https://github.com/zoom/zoom-erp-chatbot-sample) |
| **Task Manager** | Full CRUD app | [GitHub](https://github.com/zoom/task-manager-sample) |
**See**: [Sample Applications Guide](references/samples.md) for analysis of all 10 samples
## Common Operations
### Send Message to Channel
```javascript
// 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
```javascript
// 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
```javascript
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
```bash
# 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/webhook
```
### Production Deployment
**See**: [Deployment Guide](concepts/deployment.md) 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](concepts/security.md)
## 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](get-started.md)
- Fast troubleshooting first: [5-Minute Runbook](RUNBOOK.md)
### Path 1: Team Chat API (User-Level Messaging)
For sending messages as a user account.
1. [API Selection Guide](concepts/api-selection.md) - Confirm Team Chat API is right
2. [Environment Setup](concepts/environment-setup.md) - Get credentials
3. [OAuth Setup Example](examples/oauth-setup.md) - Implement authentication
4. [Send Message Example](examples/send-message.md) - Send your first message
### Path 2: Chatbot API (Interactive Bots)
For building interactive chatbots with rich messages.
1. [API Selection Guide](concepts/api-selection.md) - Confirm Chatbot API is right
2. [Environment Setup](concepts/environment-setup.md) - Get credentials (including Bot JID)
3. [Webhook Architecture](concepts/webhooks.md) - Understand webhook events
4. [Chatbot Setup Example](examples/chatbot-setup.md) - Build your first bot
5. [Message Cards Reference](references/message-cards.md) - Create rich messages
## Core Concepts
Essential understanding for both APIs.
| Document | Description |
|----------|-------------|
| [API Selection Guide](concepts/api-selection.md) | Choose Team Chat API vs Chatbot API |
| [Environment Setup](concepts/environment-setup.md) | Complete credentials and app configuration |
| [Authentication Flows](concepts/authentication.md) | OAuth vs Client Credentials |
| [Webhook Architecture](concepts/webhooks.md) | How webhooks work (Chatbot API) |
| [Message Card Structure](concepts/message-structure.md) | Card component hierarchy |
| [Deployment Guide](concepts/deployment.md) | Production deployment strategies |
| [Security Best Practices](concepts/security.md) | Secure your integration |
## Complete Examples
Working code for common scenarios.
### Authentication
| Example | Description |
|---------|-------------|
| [OAuth Setup](examples/oauth-setup.md) | User OAuth flow implementation |
| [Token Management](examples/token-management.md) | Refresh tokens, expiration handling |
### Basic Operations
| Example | Description |
|---------|-------------|
| [Send Message](examples/send-message.md) | Team Chat API message sending |
| [Chatbot Setup](examples/chatbot-setup.md) | Complete chatbot with webhooks |
| [List Channels](examples/channel-management.md) | Get user's channels |
| [Create Channel](examples/channel-management.md) | Create public/private channels |
### Interactive Features (Chatbot API)
| Example | Description |
|---------|-------------|
| [Button Actions](examples/button-actions.md) | Handle button clicks |
| [Form Submissions](examples/form-submissions.md) | Process form data |
| [Slash Commands](examples/slash-commands.md) | Create custom commands |
| [Dropdown Selects](examples/dropdown-selects.md) | Channel/member pickers |
### Advanced Integration
| Example | Description |
|---------|-------------|
| [LLM Integration](examples/llm-integration.md) | Integrate Claude/GPT |
| [Scheduled Alerts](examples/scheduled-alerts.md) | Cron + incoming webhooks |
| [Database Integration](examples/database-integration.md) | Store conversation state |
| [Multi-Step Workflows](examples/multi-step-workflows.md) | Complex user interactions |
## References
### API Documentation
| Reference | Description |
|-----------|-------------|
| [API Reference](references/api-reference.md) | Pointers and common endpoints |
| [Webhook Events](references/webhook-events.md) | Event types and handling checklist |
| [Message Cards](references/message-cards.md) | All card components |
| [Error Codes](references/error-codes.md) | Error handling guide |
### Sample Applications
| Reference | Description |
|-----------|-------------|
| [Sample Applications](references/samples.md) | Sample app index/notes |
### Field Guides
| Reference | Description |
|-----------|-------------|
| [JID Formats](references/jid-formats.md) | Understanding JID identifiers |
| [Scopes Reference](references/scopes.md) | Common scopes |
| [Rate Limits](references/rate-limits.md) | Throttling guidance |
## Troubleshooting
| Guide | Description |
|-------|-------------|
| [Common Issues](troubleshooting/common-issues.md) | Quick diagnostics and solutions |
| [OAuth Issues](troubleshooting/oauth-issues.md) | Authentication failures |
| [Webhook Issues](troubleshooting/webhook-issues.md) | Webhook debugging |
| [Message Issues](troubleshooting/message-issues.md) | Message sending problems |
| [Deployment Issues](troubleshooting/deployment-issues.md) | Production problems |
## Architecture Patterns
### Chatbot Lifecycle
```
User Action → Webhook → Process → Response
```
### LLM Integration Pattern
```
User Input → Chatbot receives → Call LLM → Send response
```
### Approval Workflow Pattern
```
Request → Send card with buttons → User clicks → Update status → Notify
```
## Common 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](concepts/api-selection.md)
- [ ] Complete [Environment Setup](concepts/environment-setup.md)
- [ ] Obtain Client ID, Client Secret
- [ ] Add required scopes
- [ ] Implement OAuth flow
- [ ] Send first message
### For Chatbot API
- [ ] Read [API Selection Guide](concepts/api-selection.md)
- [ ] Complete [Environment Setup](concepts/environment-setup.md)
- [ ] 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](references/environment-variables.md) for standardized `.env` keys and where to find each value.
troubleshooting/common-issues.md
# 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/token` for token exchange.
- Do not use `https://zoom.us/oauth/token` for chatbot token requests.
Quick check:
```bash
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**:
```javascript
// 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**:
```bash
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**:
```javascript
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**:
```javascript
{
"plainToken": "xyz123",
"encryptedToken": "hmac_sha256_hash"
}
```
**Incorrect**:
```javascript
{ "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**:
```bash
# In Zoom Team Chat, type:
/yourbot test
# Should see webhook in server logs
```
## Bot 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`**
```javascript
// Use toJid from webhook payload
await sendMessage(payload.toJid, accountId, content);
```
2. **Missing `account_id`**
```javascript
// 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**
```javascript
// ❌ Wrong
{ "text": "Hello" }
// ✅ Correct
{
"content": {
"body": [
{ "type": "message", "text": "Hello" }
]
}
}
```
### "Message truncated or garbled"
**Cause**: Special characters or exceeding 4096 char limit
**Solution**:
```javascript
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**:
```javascript
{
"type": "actions",
"items": [
{ "text": "Click Me" } // Missing value!
]
}
```
**Correct**:
```javascript
{
"type": "actions",
"items": [
{ "text": "Click Me", "value": "clicked" }
]
}
```
### "Button clicks not triggering webhooks"
**Checklist**:
- [ ] Webhook handler has `interactive_message_actions` case
- [ ] 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:
```javascript
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**
```bash
# Verify all vars exist
echo $ZOOM_CLIENT_ID
echo $ZOOM_CLIENT_SECRET
echo $ZOOM_BOT_JID
```
2. **HTTP instead of HTTPS**
- Production MUST use HTTPS
- Zoom rejects HTTP endpoints
3. **Port binding issues**
```javascript
// 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/.env` and `project/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 `curl` before treating this as server failure.
## Rate Limiting
### "Rate limit exceeded"
**Zoom Limits**:
- 10 requests/second per user
- 100 requests/second per app
**Solution**:
```javascript
// 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
```javascript
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
```javascript
// 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
```javascript
// 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](https://github.com/zoom?q=chatbot)
### Where to Get Help
- [Zoom Developer Forum](https://devforum.zoom.us/)
- [GitHub Issues](https://github.com/zoom/chatbot-nodejs-quickstart/issues)
- [Developer Support](https://devsupport.zoom.us)
### 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](../concepts/webhooks.md) - Deep dive into webhooks
- [Chatbot Setup](../examples/chatbot-setup.md) - Complete working example
- [API Reference](../references/api-reference.md) - Endpoint documentation
troubleshooting/deployment-issues.md
# 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/.env` and `chatbot-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-demo` and `/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.
troubleshooting/message-issues.md
# 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.
troubleshooting/oauth-issues.md
# 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 `code` server-side.
- Verify `state` is validated and not expired.
- Verify token is persisted where your UI expects it (session/database/local storage for demo).
troubleshooting/webhook-issues.md
# 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).