Skill 139 · Build Zoom Team Chat App
Subchapter 139.33
troubleshooting/common-issues.mdMarkdown10 KBView on GitHub
Quick diagnostics and solutions for Zoom Team Chat development.
Cause: Incorrect credentials or using wrong environment (dev vs production)
Solution:
.env match Zoom MarketplaceCause: Using wrong token endpoint.
Fix:
https://zoom.us/oauth/token for token exchange.https://zoom.us/oauth/token for chatbot token requests.Quick check:
curl -X POST https://zoom.us/oauth/token \
-H "Authorization: Basic <base64(client_id:client_secret)>" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials"Cause: Access token has expired (1 hour for user tokens)
Solution:
// Implement token refresh
if (error.message.includes('token expired')) {
const newToken = await refreshAccessToken(refreshToken);
// Retry request with new token
}Cause: Missing required scope in app configuration
Solution:
chat_message:write)Expected Behavior: This is NORMAL
Explanation: Webhooks are POST-only. Browsers send GET requests.
Test properly:
WEBHOOK_BASE_URL="http://YOUR_DEV_HOST:4000"
# Use POST instead
curl -X POST "$WEBHOOK_BASE_URL/webhook" \
-H "Content-Type: application/json" \
-d '{"event":"test"}'Cause: Mismatch between your Secret Token and Zoom’s
Solution:
ZOOM_VERIFICATION_TOKEN in .envDebug:
console.log('Expected token:', process.env.ZOOM_VERIFICATION_TOKEN);
console.log('Signature from Zoom:', req.headers['x-zm-signature']);Cause: Incorrect response format
Correct response:
{
"plainToken": "xyz123",
"encryptedToken": "hmac_sha256_hash"
}Incorrect:
{ "success": true } // Wrong!Checklist:
ngrok http 4000node server.jsTest:
# In Zoom Team Chat, type:
/yourbot test
# Should see webhook in server logsCause: Chatbot feature not enabled
Solution:
Cause: Wrong Bot JID format or environment mismatch
Solution:
v1abc123xyz@xmpp.zoom.usCommon causes:
Wrong to_jid
// Use toJid from webhook payload
await sendMessage(payload.toJid, accountId, content);Missing account_id
// Required for chatbot messages
{
"account_id": process.env.ZOOM_ACCOUNT_ID, // Don't forget!
"robot_jid": process.env.ZOOM_BOT_JID,
"to_jid": toJid
}Incorrect content format
// ❌ Wrong
{ "text": "Hello" }
// ✅ Correct
{
"content": {
"body": [
{ "type": "message", "text": "Hello" }
]
}
}Cause: Special characters or exceeding 4096 char limit
Solution:
function sanitizeMessage(message) {
return message
.trim()
.replace(/[\x00-\x1F\x7F]/g, '') // Remove control chars
.substring(0, 4096); // Enforce limit
}Cause: Missing value field
Incorrect:
{
"type": "actions",
"items": [
{ "text": "Click Me" } // Missing value!
]
}Correct:
{
"type": "actions",
"items": [
{ "text": "Click Me", "value": "clicked" }
]
}Checklist:
interactive_message_actions caseCause: Free ngrok URLs expire after 2 hours
Solutions:
Free plan behavior: URL changes each time
Solutions:
const WEBHOOK_URL = process.env.WEBHOOK_URL || 'https://YOUR_PUBLIC_WEBHOOK_URL/webhook';Common causes:
Environment variables not set
# Verify all vars exist
echo $ZOOM_CLIENT_ID
echo $ZOOM_CLIENT_SECRET
echo $ZOOM_BOT_JIDHTTP instead of HTTPS
Port binding issues
// Use PORT from environment
const PORT = process.env.PORT || 4000;Credentials exist, but wrong .env file is loaded
project/team-chat-api/.env and project/chatbot-api/.env), make sure runtime loads those files explicitly.Cause: Route mismatch between old and new demo structure.
Fix:
/team-chat/user-demo/team-chat/bot-demo/api/channel/list/api/channel/messages/api/channel/messageCause: Browser extension/adblock/privacy filter blocked a request.
What to do:
curl before treating this as server failure.Zoom Limits:
Solution:
// Implement exponential backoff
async function retryWithBackoff(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (error.status === 429) {
const delay = Math.pow(2, i) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
} else {
throw error;
}
}
}
throw new Error('Max retries exceeded');
}Cause: Team Chat surface not enabled
Solution:
Cause: App not in Local Test or not published
Solutions:
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 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-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)}...`);
}
});