HEX
Server: Apache/2.4.46 (Win64) OpenSSL/1.1.1j PHP/8.4.25
System: Windows NT DESKTOP-4TAV2RJ 10.0 build 19045 (Windows 10) AMD64
User: fred (0)
PHP: 8.4.25
Disabled: NONE
Upload Files
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)