Building a Slack Bot to Report Real-Time CRM Lead Updates

Building a Slack Bot to Report Real-Time CRM Lead Updates

The Problem

Sales teams lose valuable response time when new leads sit unnoticed in your CRM. Reps check the CRM dashboard sporadically, missing hot leads that came in minutes ago. Email notifications get buried in crowded inboxes, and by the time someone sees a new opportunity, competitors have already made contact. Your team needs instant visibility into lead activity—new signups, demo requests, high-value prospects—delivered directly where they already communicate: Slack. However, building this integration requires handling webhook authentication, parsing CRM payloads with varying structures, formatting rich Slack messages with buttons and fields, managing rate limits, and ensuring the bot doesn’t spam channels with irrelevant updates. You also need bidirectional communication—allowing reps to claim leads or update statuses directly from Slack. This tutorial provides production-ready code for a Slack bot that receives real-time webhooks from HubSpot CRM and posts formatted notifications with interactive elements.

Tech Stack & Prerequisites

  • Node.js v18+ with npm
  • Express.js 4.18+ for webhook server
  • Slack Workspace with admin permissions to install apps
  • Slack App created at api.slack.com/apps
  • HubSpot Account (free tier works) with API access
  • @slack/web-api 6.9+ (Slack SDK for Node.js)
  • @slack/bolt 3.14+ (framework for Slack apps)
  • dotenv for environment variables
  • ngrok or similar for local webhook testing (development only)
  • crypto (built-in Node.js module) for signature verification

Required Slack Setup:

  • Slack Bot created with scopes: chat:write, channels:read, chat:write.public
  • Bot Token (xoxb-…) from OAuth & Permissions
  • Signing Secret from Basic Information
  • Bot installed to your workspace

Required HubSpot Setup:

  • Workflow created in HubSpot to trigger on contact creation/update
  • Webhook action configured in workflow pointing to your server
  • Private App or API Key for optional bidirectional updates

Step-by-Step Implementation

Step 1: Setup

Initialize the project:

bash
mkdir slack-crm-bot
cd slack-crm-bot
npm init -y
npm install @slack/bolt @slack/web-api express dotenv
npm install --save-dev nodemon

Create project structure:

bash
mkdir src webhooks utils
touch src/app.js src/slackClient.js webhooks/hubspot.js utils/formatters.js .env .gitignore
```

Your structure should be:
```
slack-crm-bot/
├── src/
│   ├── app.js
│   ├── slackClient.js
├── webhooks/
│   └── hubspot.js
├── utils/
│   └── formatters.js
├── .env
├── .gitignore
└── package.json

Update .gitignore:

bash
echo "node_modules/
.env
*.log
.DS_Store" > .gitignore

package.json — Add scripts and type module:

json
{
  "name": "slack-crm-bot",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "node src/app.js",
    "dev": "nodemon src/app.js"
  },
  "dependencies": {
    "@slack/bolt": "^3.14.0",
    "@slack/web-api": "^6.9.0",
    "express": "^4.18.2",
    "dotenv": "^16.3.1"
  },
  "devDependencies": {
    "nodemon": "^3.0.1"
  }
}

Step 2: Configuration

.env — Store credentials securely:

env
# Slack Configuration
SLACK_BOT_TOKEN=xoxb-your-bot-token-here
SLACK_SIGNING_SECRET=your-signing-secret-here
SLACK_CHANNEL_ID=C01234567  # Get from Slack channel details

# HubSpot Configuration
HUBSPOT_WEBHOOK_SECRET=your_custom_webhook_secret_here

# Server Configuration
PORT=3000
NODE_ENV=development

# Optional: HubSpot API for bidirectional updates
HUBSPOT_API_KEY=your_hubspot_api_key_here

How to get Slack credentials:

  1. Go to https://api.slack.com/apps
  2. Create new app → From scratch
  3. Name it “CRM Lead Bot” and select workspace
  4. Go to OAuth & Permissions → Add Bot Token Scopes:
    • chat:write
    • channels:read
    • chat:write.public
  5. Install to workspace → Copy Bot User OAuth Token (starts with xoxb-)
  6. Go to Basic Information → Copy Signing Secret

How to get Slack Channel ID:

bash
# In Slack, right-click the channel → View channel details → Copy link
# The ID is the last part: https://yourworkspace.slack.com/archives/C01234567

src/slackClient.js — Initialize Slack client:

javascript
import { App } from '@slack/bolt';
import { WebClient } from '@slack/web-api';
import dotenv from 'dotenv';

dotenv.config();

// Initialize Slack App with Bolt framework
export const slackApp = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  socketMode: false, // Using HTTP endpoints, not socket mode
});

// Initialize Web API client for direct API calls
export const slackClient = new WebClient(process.env.SLACK_BOT_TOKEN);

// Test connection and get bot info
export async function testSlackConnection() {
  try {
    const auth = await slackClient.auth.test();
    console.log('✓ Slack connected successfully');
    console.log(`  Bot User: @${auth.user}`);
    console.log(`  Workspace: ${auth.team}`);
    return true;
  } catch (error) {
    console.error('✗ Slack connection failed:', error.message);
    return false;
  }
}

// Post a message to a channel
export async function postToChannel(channelId, blocks, text) {
  try {
    const result = await slackClient.chat.postMessage({
      channel: channelId,
      blocks: blocks,
      text: text, // Fallback text for notifications
    });
    
    return result;
  } catch (error) {
    console.error('Error posting to Slack:', error.message);
    throw error;
  }
}

// Update a message (for interactive elements)
export async function updateMessage(channelId, messageTs, blocks) {
  try {
    const result = await slackClient.chat.update({
      channel: channelId,
      ts: messageTs,
      blocks: blocks,
    });
    
    return result;
  } catch (error) {
    console.error('Error updating Slack message:', error.message);
    throw error;
  }
}

// Add reaction to message
export async function addReaction(channelId, messageTs, emoji) {
  try {
    await slackClient.reactions.add({
      channel: channelId,
      timestamp: messageTs,
      name: emoji,
    });
  } catch (error) {
    console.error('Error adding reaction:', error.message);
  }
}

Step 3: Core Logic

utils/formatters.js — Format CRM data into Slack Block Kit messages:

javascript
// Format lead data into rich Slack message blocks
export function formatNewLeadMessage(lead) {
  const firstName = lead.firstname || 'Unknown';
  const lastName = lead.lastname || '';
  const email = lead.email || 'No email provided';
  const phone = lead.phone || 'No phone';
  const company = lead.company || 'No company';
  const leadSource = lead.hs_analytics_source || 'Unknown source';
  const leadScore = lead.hubspotscore || 'N/A';
  
  // Determine priority based on lead score
  let priorityEmoji = '🟢';
  let priorityText = 'Low';
  
  if (leadScore !== 'N/A') {
    const score = parseInt(leadScore);
    if (score >= 80) {
      priorityEmoji = '🔴';
      priorityText = 'High';
    } else if (score >= 50) {
      priorityEmoji = '🟡';
      priorityText = 'Medium';
    }
  }
  
  const blocks = [
    {
      type: 'header',
      text: {
        type: 'plain_text',
        text: `${priorityEmoji} New Lead: ${firstName} ${lastName}`,
        emoji: true,
      },
    },
    {
      type: 'section',
      fields: [
        {
          type: 'mrkdwn',
          text: `*Email:*\n${email}`,
        },
        {
          type: 'mrkdwn',
          text: `*Phone:*\n${phone}`,
        },
        {
          type: 'mrkdwn',
          text: `*Company:*\n${company}`,
        },
        {
          type: 'mrkdwn',
          text: `*Source:*\n${leadSource}`,
        },
        {
          type: 'mrkdwn',
          text: `*Priority:*\n${priorityEmoji} ${priorityText}`,
        },
        {
          type: 'mrkdwn',
          text: `*Lead Score:*\n${leadScore}`,
        },
      ],
    },
    {
      type: 'divider',
    },
    {
      type: 'actions',
      elements: [
        {
          type: 'button',
          text: {
            type: 'plain_text',
            text: '👤 Claim Lead',
            emoji: true,
          },
          style: 'primary',
          value: lead.hs_object_id || lead.vid,
          action_id: 'claim_lead',
        },
        {
          type: 'button',
          text: {
            type: 'plain_text',
            text: '📞 Schedule Call',
            emoji: true,
          },
          value: lead.hs_object_id || lead.vid,
          action_id: 'schedule_call',
        },
        {
          type: 'button',
          text: {
            type: 'plain_text',
            text: '🗑️ Dismiss',
            emoji: true,
          },
          style: 'danger',
          value: lead.hs_object_id || lead.vid,
          action_id: 'dismiss_lead',
        },
      ],
    },
  ];
  
  // Add HubSpot link if object ID available
  if (lead.hs_object_id) {
    blocks.push({
      type: 'context',
      elements: [
        {
          type: 'mrkdwn',
          text: `<https://app.hubspot.com/contacts/your-hub-id/contact/${lead.hs_object_id}|View in HubSpot>`,
        },
      ],
    });
  }
  
  return blocks;
}

// Format lead update message
export function formatLeadUpdateMessage(lead, changes) {
  const firstName = lead.firstname || 'Unknown';
  const lastName = lead.lastname || '';
  
  const changesList = changes.map(change => 
    `• *${change.field}*: ${change.oldValue || 'empty'}${change.newValue}`
  ).join('\n');
  
  const blocks = [
    {
      type: 'section',
      text: {
        type: 'mrkdwn',
        text: `*Lead Updated:* ${firstName} ${lastName}\n\n${changesList}`,
      },
    },
  ];
  
  return blocks;
}

// Format message after button interaction
export function formatClaimedLeadMessage(lead, claimedBy) {
  const firstName = lead.firstname || 'Unknown';
  const lastName = lead.lastname || '';
  
  const blocks = [
    {
      type: 'section',
      text: {
        type: 'mrkdwn',
        text: `✅ *Lead Claimed*\n${firstName} ${lastName} is now assigned to <@${claimedBy}>`,
      },
    },
  ];
  
  return blocks;
}

webhooks/hubspot.js — Handle HubSpot webhook payloads:

javascript
import express from 'express';
import crypto from 'crypto';
import { postToChannel } from '../src/slackClient.js';
import { formatNewLeadMessage } from '../utils/formatters.js';

const router = express.Router();

// Verify HubSpot webhook signature (optional but recommended)
function verifyHubSpotSignature(req) {
  const signature = req.headers['x-hubspot-signature'];
  const webhookSecret = process.env.HUBSPOT_WEBHOOK_SECRET;
  
  if (!webhookSecret || !signature) {
    return true; // Skip verification if not configured
  }
  
  const requestBody = JSON.stringify(req.body);
  const hash = crypto
    .createHmac('sha256', webhookSecret)
    .update(requestBody)
    .digest('hex');
  
  return hash === signature;
}

// Parse HubSpot contact properties from webhook payload
function parseContactProperties(payload) {
  // HubSpot sends properties in different formats depending on webhook type
  const properties = payload.properties || {};
  
  // Extract common fields
  const contact = {
    hs_object_id: payload.objectId || payload.vid,
    firstname: properties.firstname?.value || properties.firstname,
    lastname: properties.lastname?.value || properties.lastname,
    email: properties.email?.value || properties.email,
    phone: properties.phone?.value || properties.phone,
    company: properties.company?.value || properties.company,
    hs_analytics_source: properties.hs_analytics_source?.value || properties.hs_analytics_source,
    hubspotscore: properties.hubspotscore?.value || properties.hubspotscore,
  };
  
  return contact;
}

// POST /webhooks/hubspot/contact-created
router.post('/contact-created', async (req, res) => {
  try {
    console.log('\n📥 Received HubSpot webhook: contact-created');
    
    // Verify signature
    if (!verifyHubSpotSignature(req)) {
      console.error('Invalid webhook signature');
      return res.status(401).json({ error: 'Invalid signature' });
    }
    
    // Parse contact data
    const contact = parseContactProperties(req.body);
    
    console.log(`  Contact: ${contact.firstname} ${contact.lastname}`);
    console.log(`  Email: ${contact.email}`);
    
    // Format Slack message
    const blocks = formatNewLeadMessage(contact);
    const fallbackText = `New lead: ${contact.firstname} ${contact.lastname} (${contact.email})`;
    
    // Post to Slack
    await postToChannel(
      process.env.SLACK_CHANNEL_ID,
      blocks,
      fallbackText
    );
    
    console.log('✓ Posted to Slack successfully');
    
    // Respond to HubSpot webhook
    res.status(200).json({ success: true });
    
  } catch (error) {
    console.error('Error processing webhook:', error.message);
    res.status(500).json({ error: error.message });
  }
});

// POST /webhooks/hubspot/contact-updated
router.post('/contact-updated', async (req, res) => {
  try {
    console.log('\n📥 Received HubSpot webhook: contact-updated');
    
    const contact = parseContactProperties(req.body);
    
    // Only notify for high-priority changes
    const importantFields = ['hubspotscore', 'lifecyclestage', 'hs_lead_status'];
    const hasImportantChange = Object.keys(req.body.properties || {}).some(
      field => importantFields.includes(field)
    );
    
    if (!hasImportantChange) {
      console.log('  Skipping - no important field changes');
      return res.status(200).json({ success: true, skipped: true });
    }
    
    console.log(`  Contact updated: ${contact.firstname} ${contact.lastname}`);
    
    // Post notification (simplified for updates)
    const blocks = [
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `🔄 *Lead Updated:* ${contact.firstname} ${contact.lastname}\nLead score: ${contact.hubspotscore}`,
        },
      },
    ];
    
    await postToChannel(
      process.env.SLACK_CHANNEL_ID,
      blocks,
      `Lead updated: ${contact.firstname} ${contact.lastname}`
    );
    
    res.status(200).json({ success: true });
    
  } catch (error) {
    console.error('Error processing update webhook:', error.message);
    res.status(500).json({ error: error.message });
  }
});

export default router;

src/app.js — Main application with Slack interactivity:

javascript
import express from 'express';
import dotenv from 'dotenv';
import { slackApp, testSlackConnection, updateMessage, addReaction } from './slackClient.js';
import hubspotWebhooks from '../webhooks/hubspot.js';
import { formatClaimedLeadMessage } from '../utils/formatters.js';

dotenv.config();

const app = express();
const PORT = process.env.PORT || 3000;

// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Health check
app.get('/health', (req, res) => {
  res.json({ status: 'healthy', timestamp: new Date().toISOString() });
});

// HubSpot webhooks
app.use('/webhooks/hubspot', hubspotWebhooks);

// Slack interactive endpoints - handle button clicks
slackApp.action('claim_lead', async ({ ack, body, client }) => {
  await ack();
  
  try {
    const userId = body.user.id;
    const leadId = body.actions[0].value;
    const messageTs = body.message.ts;
    const channelId = body.channel.id;
    
    console.log(`\n👤 Lead ${leadId} claimed by user ${userId}`);
    
    // Update the message to show claimed status
    const claimedBlocks = [
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `✅ *Lead Claimed*\nThis lead has been claimed by <@${userId}>`,
        },
      },
      {
        type: 'context',
        elements: [
          {
            type: 'mrkdwn',
            text: `Lead ID: ${leadId}`,
          },
        ],
      },
    ];
    
    await updateMessage(channelId, messageTs, claimedBlocks);
    
    // Add checkmark reaction
    await addReaction(channelId, messageTs, 'white_check_mark');
    
    // TODO: Update CRM with owner assignment via API
    // await assignLeadInHubSpot(leadId, userId);
    
  } catch (error) {
    console.error('Error handling claim_lead action:', error);
  }
});

slackApp.action('schedule_call', async ({ ack, body, client }) => {
  await ack();
  
  try {
    const userId = body.user.id;
    const leadId = body.actions[0].value;
    
    console.log(`\n📞 Call scheduled for lead ${leadId} by user ${userId}`);
    
    // Send ephemeral message (only visible to user who clicked)
    await client.chat.postEphemeral({
      channel: body.channel.id,
      user: userId,
      text: `📅 Opening calendar to schedule call for lead ${leadId}...`,
    });
    
    // TODO: Integrate with calendar API (Google Calendar, Calendly, etc.)
    
  } catch (error) {
    console.error('Error handling schedule_call action:', error);
  }
});

slackApp.action('dismiss_lead', async ({ ack, body, client }) => {
  await ack();
  
  try {
    const userId = body.user.id;
    const leadId = body.actions[0].value;
    const messageTs = body.message.ts;
    const channelId = body.channel.id;
    
    console.log(`\n🗑️ Lead ${leadId} dismissed by user ${userId}`);
    
    // Delete the message
    await client.chat.delete({
      channel: channelId,
      ts: messageTs,
    });
    
    // Send confirmation
    await client.chat.postEphemeral({
      channel: channelId,
      user: userId,
      text: `Lead ${leadId} has been dismissed.`,
    });
    
  } catch (error) {
    console.error('Error handling dismiss_lead action:', error);
  }
});

// Start Slack app receiver (for interactive components)
const slackReceiver = slackApp.receiver;

// Mount Slack events to Express
app.use('/slack/events', slackReceiver.router);

// Start server
async function startServer() {
  try {
    // Test Slack connection
    await testSlackConnection();
    
    app.listen(PORT, () => {
      console.log(`\n🚀 Server running on http://localhost:${PORT}`);
      console.log(`\nWebhook endpoints:`);
      console.log(`  POST /webhooks/hubspot/contact-created`);
      console.log(`  POST /webhooks/hubspot/contact-updated`);
      console.log(`\nSlack events:`);
      console.log(`  POST /slack/events\n`);
      
      if (process.env.NODE_ENV === 'development') {
        console.log('💡 For local testing, expose with ngrok:');
        console.log(`   ngrok http ${PORT}\n`);
      }
    });
    
  } catch (error) {
    console.error('Failed to start server:', error.message);
    process.exit(1);
  }
}

startServer();

Step 4: Testing

Test 1: Start the Server

bash
npm run dev
```

Expected output:
```
✓ Slack connected successfully
  Bot User: @CRM Lead Bot
  Workspace: YourWorkspace

🚀 Server running on http://localhost:3000

Webhook endpoints:
  POST /webhooks/hubspot/contact-created
  POST /webhooks/hubspot/contact-updated

Test 2: Expose Local Server with ngrok (for webhook testing)

bash
# Install ngrok if you haven't: https://ngrok.com/download
ngrok http 3000

Copy the HTTPS forwarding URL (e.g., https://abc123.ngrok.io)

Test 3: Configure HubSpot Workflow

  1. In HubSpot, go to AutomationWorkflows
  2. Create workflow: Contact-basedBlank workflow
  3. Set enrollment trigger: Contact is created
  4. Add action: Send webhook
    • Method: POST
    • URL: https://your-ngrok-url.ngrok.io/webhooks/hubspot/contact-created
    • Include all contact properties
  5. Turn workflow ON

[SCREENSHOT SUGGESTION: HubSpot workflow builder showing webhook action configuration with URL field]

Test 4: Create Test Contact in HubSpot

bash
# Using HubSpot UI: Contacts → Create contact
# Or via curl with HubSpot API:

curl -X POST https://api.hubapi.com/crm/v3/objects/contacts \
  -H "Authorization: Bearer YOUR_HUBSPOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "properties": {
      "email": "test@example.com",
      "firstname": "John",
      "lastname": "Doe",
      "phone": "+1234567890",
      "company": "Test Corp"
    }
  }'

Expected Result:

  • Server logs show: 📥 Received HubSpot webhook: contact-created
  • Slack channel receives formatted message with lead details
  • Interactive buttons appear: “Claim Lead”, “Schedule Call”, “Dismiss”

Test 5: Test Interactive Buttons

  1. Click “Claim Lead” button in Slack
  2. Verify message updates to show claimed status
  3. Check server logs: 👤 Lead claimed by user U01234567

Test 6: Manual Webhook Test (without HubSpot)

bash
curl -X POST http://localhost:3000/webhooks/hubspot/contact-created \
  -H "Content-Type: application/json" \
  -d '{
    "objectId": "12345",
    "properties": {
      "firstname": "Jane",
      "lastname": "Smith",
      "email": "jane@example.com",
      "phone": "+1987654321",
      "company": "Acme Inc",
      "hubspotscore": "85",
      "hs_analytics_source": "ORGANIC_SEARCH"
    }
  }'

Testing Checklist:

  • ✓ Server starts without errors
  • ✓ Slack connection verified
  • ✓ Webhook receives HubSpot payload
  • ✓ Message posts to correct Slack channel
  • ✓ Block Kit formatting displays correctly
  • ✓ Buttons are clickable and functional
  • ✓ Message updates after button click
  • ✓ High-priority leads show red indicator

Test 7: Monitor Logs

bash
# Server logs should show:
✓ Slack connected successfully
📥 Received HubSpot webhook: contact-created
  Contact: Jane Smith
  Email: jane@example.com
✓ Posted to Slack successfully
```

## Common Errors & Troubleshooting

### Error 1: "not_in_channel" When Posting Message

**Problem:** Slack returns error: `An API error occurred: not_in_channel`.

**Solution:** The bot needs to be added to the channel before it can post. Fix this two ways:

**Method 1 - Add bot to channel manually:**
```
1. In Slack, go to the channel
2. Type: /invite @CRM Lead Bot
3. Press Enter
```

**Method 2 - Use `chat:write.public` scope (already included):**
This allows the bot to post to any public channel without being added. Verify the scope in your Slack App settings at api.slack.com/apps → OAuth & Permissions.

If still failing, reinstall the app to workspace:
```
1. Go to api.slack.com/apps → Your App
2. OAuth & Permissions → Reinstall to Workspace
3. Update SLACK_BOT_TOKEN in .env with new token
```

### Error 2: Slack Interactive Buttons Not Working

**Problem:** Clicking buttons shows "Action failed" or nothing happens.

**Solution:** Slack needs to know where to send button click events. Configure Interactivity:
```
1. Go to api.slack.com/apps → Your App
2. Interactivity & Shortcuts → Turn ON
3. Request URL: https://your-ngrok-url.ngrok.io/slack/events
4. Save Changes

Critical: The Request URL must be publicly accessible. In production, use your actual domain. During development, use ngrok.

Verify with Slack’s Event URL tester:

  • After saving, Slack sends a challenge request
  • Your server must respond with the challenge value
  • The Bolt framework handles this automatically

Debug interactive actions:

javascript
// Add debugging to action handler
slackApp.action('claim_lead', async ({ ack, body, client }) => {
  console.log('Received action:', JSON.stringify(body, null, 2));
  await ack();
  // ... rest of handler
});

Error 3: “invalid_signature” from HubSpot Webhook

Problem: Server responds with 401 for valid HubSpot webhooks.

Solution: Signature verification is failing. This happens when:

Issue 1 – Wrong secret in .env:

env
# Make sure this matches what you set in HubSpot workflow
HUBSPOT_WEBHOOK_SECRET=your_custom_secret_here

Issue 2 – HubSpot doesn’t send signatures by default: HubSpot workflows don’t include signatures. You need to configure it manually or disable verification for HubSpot:

javascript
// In webhooks/hubspot.js, modify verification:
function verifyHubSpotSignature(req) {
  // Skip verification for HubSpot workflows (they don't sign by default)
  if (process.env.NODE_ENV === 'development') {
    return true;
  }
  
  // Only verify if signature header exists
  const signature = req.headers['x-hubspot-signature'];
  if (!signature) {
    console.warn('No signature provided - skipping verification');
    return true;
  }
  
  // ... rest of verification code
}

Issue 3 – Body already parsed: Express middleware must preserve raw body for signature verification:

javascript
// In app.js, add this BEFORE other middleware:
app.use('/webhooks/hubspot', express.raw({ type: 'application/json' }));

For production, use webhook signature verification with HubSpot API v3: Create a custom integration app in HubSpot that includes webhook signatures, or validate the request came from HubSpot IP ranges.

Security Checklist

Protect your integration with these critical practices:

  • Rotate Slack tokens regularly — Bot tokens don’t expire, but should be rotated quarterly. Generate new token in OAuth & Permissions → Reinstall App.
  • Validate webhook signatures — Always verify HubSpot webhook requests to prevent unauthorized posting. Implement IP allowlisting for HubSpot’s webhook IPs:
javascript
  const HUBSPOT_IPS = ['52.73.140.222', '54.166.76.185', '54.87.211.127'];
  
  app.use('/webhooks/hubspot', (req, res, next) => {
    const clientIp = req.ip || req.connection.remoteAddress;
    if (!HUBSPOT_IPS.includes(clientIp) && process.env.NODE_ENV === 'production') {
      return res.status(403).json({ error: 'Unauthorized IP' });
    }
    next();
  });
  • Use environment-specific channels — Never post test leads to production sales channels. Create separate channels for dev/staging:
env
  # .env.production
  SLACK_CHANNEL_ID=C_PRODUCTION_SALES
  
  # .env.development  
  SLACK_CHANNEL_ID=C_DEV_TESTING
  • Implement rate limiting — Prevent webhook flooding from overwhelming Slack:
javascript
  import rateLimit from 'express-rate-limit';
  
  const webhookLimiter = rateLimit({
    windowMs: 60 * 1000, // 1 minute
    max: 50, // max 50 requests per minute
    message: { error: 'Too many webhook requests' }
  });
  
  app.use('/webhooks/', webhookLimiter);
  • Sanitize CRM data before posting — Never expose sensitive PII in Slack messages. Mask phone numbers and emails for non-admin users:
javascript
  function sanitizeForSlack(contact, userRole) {
    if (userRole !== 'admin') {
      contact.phone = contact.phone ? '***-***-' + contact.phone.slice(-4) : 'N/A';
      contact.email = contact.email ? '***@' + contact.email.split('@')[1] : 'N/A';
    }
    return contact;
  }
  • Encrypt secrets at rest — Use AWS Secrets Manager, Azure Key Vault, or HashiCorp Vault in production. Never commit .env files.
  • Log webhook activity — Track all webhook requests for audit trails:
javascript
  app.use('/webhooks', (req, res, next) => {
    console.log(`[${new Date().toISOString()}] Webhook: ${req.path} from ${req.ip}`);
    next();
  });
  • Set up Slack audit logs — Enable workspace audit logging to track bot activity and detect anomalies.
  • Use HTTPS only — In production, disable HTTP:
javascript
  if (process.env.NODE_ENV === 'production' && !req.secure) {
    return res.redirect('https://' + req.headers.host + req.url);
  }

Leave a Comment

Your email address will not be published. Required fields are marked *