File: C:/Users/fred/.codex/.tmp/plugins/plugins/zoom/skills/team-chat/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": "[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 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 an LLM provider
## 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)