Build Zoom Team Chat App
OpenAIOfficialCreated by ZoomDistributed by OpenAI
- Skill
- Tool integration
- Software development
- MIT
- Medium risk
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a systematic approach to building applications for Zoom Team Chat. It guides developers through selecting the appropriate API surface, configuring app settings, modeling message structures, and implementing secure authentication and webhook handling. It covers the lifecycle of building both user-scoped messaging integrations and interactive chatbot workflows.
What it does
- Helps select between Team Chat API and Chatbot API based on integration requirements.
- Outlines the configuration of app types, scopes, and role enablement.
- Provides guidance on modeling message structures including rich cards, forms, and slash commands.
- Details the implementation of OAuth flows and token management.
- Supports the setup of webhook handlers with signature verification.
- Assists in debugging common issues related to JID formats, channel membership, and payload shapes.
When to use it
- When you need to integrate custom messaging or automation into Zoom Team Chat.
- When designing interactive chatbot workflows with buttons, forms, or dropdowns.
- When setting up OAuth authentication for Zoom-based applications.
- When troubleshooting connectivity or message delivery issues in a Zoom integration.
Advantages
- Provides a clear, step-by-step path for Zoom app development.
- Ensures security best practices like signature verification are prioritized.
- Helps avoid common pitfalls in API selection and message formatting.
- Centralizes references for environment variables and API endpoints.
Usage examples
- “How do I start building a chatbot for Zoom Team Chat?”
- The agent will guide you through choosing the Chatbot API, configuring the bot identity, and setting up the necessary OAuth and webhook infrastructure.
- “I am getting an invalid access token error when sending messages.”
- The agent will help you verify your app type, check if the required scopes are enabled, and confirm if the user has re-consented after recent scope changes.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/openai/build-zoom-team-chat-app/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip build-zoom-team-chat-app.zip -d .agents/skills
Documentation by Zoom
Build Zoom Team Chat App
Use this skill when the target surface is Zoom Team Chat. First decide whether the integration is user-scoped messaging, a chatbot, an interactive message-card workflow, or a webhook-driven automation.
Workflow
Choose the API surface: Team Chat API for user-scoped actions, Chatbot API for bot identity and chatbot workflows.
Confirm app type, scopes, role enablement, and whether the account has Zoom for Developers enabled.
Model message structure before coding: plain messages, rich cards, buttons, dropdowns, forms, slash commands, and threaded replies.
Implement OAuth and token refresh separately from message sending, with clear storage boundaries.
Add webhook handlers for interactivity and lifecycle events with signature verification and retry-safe processing.
Debug by checking JID formats, channel membership, bot installation, scopes, role settings, and message-card payload shape.
References
Full preserved guide: references/full-guide.md
API selection: concepts/api-selection.md
Message structure: concepts/message-structure.md
Message cards: references/message-cards.md
Scopes: references/scopes.md
Chatbot setup: examples/chatbot-setup.md
Common issues: troubleshooting/common-issues.md
Files
- ATTRIBUTION.mdAdded by AutoGineer880 B
- LICENSELicence file added by AutoGineer1.1 KiB
- RUNBOOK.md2.7 KiB
- SKILL.md1.5 KiB
- agents/openai.yaml145 B
- concepts/api-selection.md7.3 KiB
- concepts/authentication.md1.4 KiB
- concepts/deployment.md719 B
- concepts/environment-setup.md8.8 KiB
- concepts/message-structure.md675 B
- concepts/security.md478 B
- concepts/webhooks.md11.8 KiB
- examples/button-actions.md393 B
- examples/channel-management.md320 B
- examples/chatbot-setup.md12.2 KiB
- examples/database-integration.md312 B
- examples/dropdown-selects.md304 B
- examples/form-submissions.md385 B
- examples/llm-integration.md532 B
- examples/multi-step-workflows.md300 B
- examples/oauth-setup.md1.6 KiB
- examples/scheduled-alerts.md352 B
- examples/send-message.md417 B
- examples/slash-commands.md509 B
- examples/token-management.md442 B
- get-started.md1.6 KiB
- references/api-reference.md538 B
- references/environment-variables.md1006 B
- references/error-codes.md448 B
- references/full-guide.md22.9 KiB
- references/jid-formats.md247 B
- references/message-cards.md6.6 KiB
- references/rate-limits.md205 B
- references/sample-comparison.md1.1 KiB
- references/samples.md14.2 KiB
- references/scopes.md340 B
- references/webhook-events.md564 B
- troubleshooting/common-issues.md9.7 KiB
- troubleshooting/deployment-issues.md641 B
- troubleshooting/message-issues.md317 B
- troubleshooting/oauth-issues.md882 B
- troubleshooting/webhook-issues.md364 B
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 responseExample flow:
1. User types "/weather San Francisco" in Zoom Team Chat
2. Zoom sends POST request to your Bot Endpoint URL
3. Your server receives webhook with payload.cmd = "San Francisco"
4. Your server calls weather API
5. Your server sends chatbot message back with weather dataWebhook Lifecycle
Setup (One-time)
Configure Bot Endpoint URL in Zoom Marketplace:
Development:
https://abc123.ngrok.io/webhookProduction:
https://yourdomain.com/webhook
Verify endpoint - Zoom sends validation request when you save the URL
Runtime (Per Event)
User action → Zoom webhook → Your handler → ResponseWebhook Events
| Event | Trigger | When It Fires |
|---|---|---|
endpoint.url_validation | URL configured/changed | Setup only |
bot_installed | Bot added to account | Installation |
bot_notification | User messages bot or uses slash command | User interaction |
interactive_message_actions | Button clicked | User clicks button |
chat_message.submit | Form submitted | User submits form |
app_deauthorized | Bot removed from account | Uninstallation |
See: Webhook Events Reference for complete event catalog
Webhook Structure
Request Headers
Every webhook includes these headers:
{
'x-zm-signature': 'v0=abc123...', // Signature for verification
'x-zm-request-timestamp': '1234567890', // Unix timestamp
'content-type': 'application/json'
}Request Body
{
"event": "bot_notification", // Event type
"payload": { // Event-specific data
"accountId": "...",
"toJid": "...",
"cmd": "...",
// ... more fields
}
}Webhook Verification
CRITICAL: Always verify webhook signatures to prevent unauthorized requests.
Why Verify?
Without verification, anyone can send fake webhooks to your endpoint, potentially:
Triggering unauthorized actions
Causing denial-of-service attacks
Accessing sensitive data
Verification Algorithm
const crypto = require('crypto');
function verifyZoomWebhookSignature(req) {
const signature = req.headers['x-zm-signature'];
const timestamp = req.headers['x-zm-request-timestamp'];
const secretToken = process.env.ZOOM_VERIFICATION_TOKEN;
if (!signature || !timestamp) {
throw new Error('Missing signature headers');
}
// Construct message
const message = `v0:${timestamp}:${JSON.stringify(req.body)}`;
// Calculate expected signature
const expectedSignature = crypto
.createHmac('sha256', secretToken)
.update(message)
.digest('hex');
// Compare signatures
if (signature !== `v0=${expectedSignature}`) {
throw new Error('Invalid webhook signature');
}
return true;
}Verification Flow
1. Extract signature and timestamp from headers
2. Construct message: "v0:{timestamp}:{JSON body}"
3. Calculate HMAC-SHA256 with secret token
4. Compare calculated signature with header signature
5. Accept if match, reject if mismatchWebhook Handler Pattern
Basic Handler
app.post('/webhook', (req, res) => {
try {
// Step 1: Verify signature
verifyZoomWebhookSignature(req);
// Step 2: Extract event and payload
const { event, payload } = req.body;
// Step 3: Handle event
switch (event) {
case 'endpoint.url_validation':
return handleUrlValidation(req, res);
case 'bot_installed':
return handleBotInstalled(payload, res);
case 'bot_notification':
return handleBotNotification(payload, res);
case 'interactive_message_actions':
return handleButtonClick(payload, res);
case 'app_deauthorized':
return handleBotUninstalled(payload, res);
default:
console.log('Unsupported event:', event);
return res.status(200).json({ success: true });
}
} catch (error) {
if (error.message.includes('signature')) {
return res.status(401).json({ error: 'Invalid webhook signature' });
}
return res.status(500).json({ error: error.message });
}
});Event Handlers
1. URL Validation (endpoint.url_validation)
Zoom sends this when you configure or change your Bot Endpoint URL.
Purpose: Verify you control the endpoint
Payload:
{
"event": "endpoint.url_validation",
"payload": {
"plainToken": "xyz123abc"
}
}Required Response:
{
"plainToken": "xyz123abc",
"encryptedToken": "hmac_sha256(plainToken, secret_token)"
}Implementation:
function handleUrlValidation(req, res) {
const { plainToken } = req.body.payload;
const encryptedToken = crypto
.createHmac('sha256', process.env.ZOOM_VERIFICATION_TOKEN)
.update(plainToken)
.digest('hex');
return res.status(200).json({
plainToken,
encryptedToken
});
}2. Bot Installed (bot_installed)
Fired when someone adds your bot to their account.
Payload:
{
"event": "bot_installed",
"payload": {
"accountId": "...",
"userId": "...",
"timestamp": 1234567890
}
}Use Case: Initialize bot state, send welcome message
Implementation:
async function handleBotInstalled(payload, res) {
console.log('Bot installed for account:', payload.accountId);
// Optional: Initialize database, send welcome message
// await initializeBotForAccount(payload.accountId);
return res.status(200).json({ success: true });
}3. Bot Notification (bot_notification)
Fired when:
User sends message to bot via slash command
User sends direct message to bot
Payload:
{
"event": "bot_notification",
"payload": {
"accountId": "...",
"toJid": "[email protected]",
"robotJid": "[email protected]",
"userJid": "[email protected]",
"cmd": "user's input text",
"userName": "John Doe",
"channelName": "Marketing",
"timestamp": 1234567890
}
}Key Fields:
cmd- User's input after the slash commandtoJid- Where to send response (channel or DM)accountId- Account identifier
Use Case: Process commands, integrate LLM, send responses
Implementation:
async function handleBotNotification(payload, res) {
const { toJid, cmd, accountId, userName } = payload;
console.log(`${userName} sent: ${cmd}`);
// Process command (e.g., call LLM)
const response = await processCommand(cmd);
// Send response
await sendChatbotMessage(toJid, accountId, {
body: [{ type: 'message', text: response }]
});
return res.status(200).json({ success: true });
}4. Interactive Message Actions (interactive_message_actions)
Fired when user clicks a button in a chatbot message.
Payload:
{
"event": "interactive_message_actions",
"payload": {
"accountId": "...",
"toJid": "...",
"actionItem": {
"text": "Approve",
"value": "approve" // This is what you check
},
"messageId": "...",
"userName": "John Doe"
}
}Key Field: actionItem.value - The button's value you defined
Implementation:
async function handleButtonClick(payload, res) {
const { actionItem, toJid, accountId, userName } = payload;
console.log(`${userName} clicked: ${actionItem.value}`);
switch (actionItem.value) {
case 'approve':
await sendChatbotMessage(toJid, accountId, {
body: [{ type: 'message', text: '✅ Approved!' }]
});
break;
case 'reject':
await sendChatbotMessage(toJid, accountId, {
body: [{ type: 'message', text: '❌ Rejected' }]
});
break;
default:
console.log('Unknown action:', actionItem.value);
}
return res.status(200).json({ success: true });
}Webhook Best Practices
1. Always Verify Signatures
// ✅ GOOD
app.post('/webhook', (req, res) => {
verifyZoomWebhookSignature(req);
// ... handle event
});
// ❌ BAD
app.post('/webhook', (req, res) => {
// No verification - vulnerable to fake webhooks!
});2. Respond Quickly
Zoom expects a 200 response within 3 seconds.
// ✅ GOOD - Respond immediately, process async
app.post('/webhook', (req, res) => {
verifyZoomWebhookSignature(req);
// Respond immediately
res.status(200).json({ success: true });
// Process asynchronously
processWebhookAsync(req.body);
});
// ❌ BAD - Slow processing blocks response
app.post('/webhook', async (req, res) => {
await slowLLMCall(); // May timeout!
res.status(200).json({ success: true });
});3. Handle All Events Gracefully
// ✅ GOOD - Handle unknown events
switch (event) {
case 'bot_notification':
return handleBotNotification(payload, res);
default:
console.log('Unsupported event:', event);
return res.status(200).json({ success: true });
}
// ❌ BAD - Crash on unknown events
switch (event) {
case 'bot_notification':
return handleBotNotification(payload, res);
// Missing default case - crashes on new events!
}4. Log Webhook Activity
app.post('/webhook', (req, res) => {
const { event, payload } = req.body;
console.log(`[Webhook] ${event}`, {
timestamp: new Date().toISOString(),
accountId: payload.accountId,
userId: payload.userId
});
// ... handle event
});5. Use Environment Variables
// ✅ GOOD
const SECRET_TOKEN = process.env.ZOOM_VERIFICATION_TOKEN;
// ❌ BAD - Hardcoded secret
const SECRET_TOKEN = 'abc123xyz';Testing Webhooks
Local Development with ngrok
# Install ngrok
npm install -g ngrok
# Expose local server
ngrok http 4000
# Copy HTTPS URL to Zoom Marketplace
# Example: https://abc123.ngrok.io/webhookManual Testing
WEBHOOK_BASE_URL="http://YOUR_DEV_HOST:4000"
# Test with curl (will fail signature verification - expected)
curl -X POST "$WEBHOOK_BASE_URL/webhook" \
-H "Content-Type: application/json" \
-d '{"event":"test"}'
# Expected response: "Invalid webhook signature" (this is correct!)Verify Webhook is Working
Success indicators:
Zoom successfully validates your endpoint URL
bot_installedevent fires when you add the botbot_notificationfires when you use slash commandButton clicks trigger
interactive_message_actions
Common Webhook Issues
| Issue | Cause | Solution |
|---|---|---|
| "Cannot GET /webhook" | Browser sends GET, webhook is POST | Normal - test with POST or Zoom |
| "Invalid signature" | Wrong secret token | Verify ZOOM_VERIFICATION_TOKEN matches Zoom Marketplace |
| URL validation fails | Response format incorrect | Return plainToken + encryptedToken |
| No webhooks received | Wrong endpoint URL | Verify URL in Zoom Marketplace matches your server |
| Webhooks timeout | Slow response | Return 200 immediately, process async |
Next Steps
Webhook Events Reference - Complete event catalog
Button Actions Example - Handle button clicks
Slash Commands Example - Process slash commands
LLM Integration Example - Integrate an LLM provider
Resources
Requirements and compatibility
Written for: OpenAI Codex
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
What our scanner noticed· Medium risk
- Possible data-exfiltration endpoints
- Instructions that use the network
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- Zoom
- Publisher
- OpenAI (Official)
- Distributed by
- OpenAI
- Source
- openai/plugins · plugins/zoom/skills/team-chat · commit d420d13
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from plugins/zoom/LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Zoom or OpenAI.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download build-zoom-team-chat-app.zip