Slack Integration for ChatGPT Apps: Complete Implementation Guide

Integrating your ChatGPT application with Slack transforms it from a standalone tool into a powerful workspace assistant accessible to millions of users. Slack's 18+ million daily active users represent a massive distribution channel, while its rich API enables sophisticated interactions through slash commands, interactive components, and real-time events.

This comprehensive guide walks you through building production-ready Slack integrations for ChatGPT apps, covering OAuth 2.1 authentication with PKCE, slash command implementation, Block Kit interactive components, event subscriptions, and deployment best practices. Whether you're building a customer support bot, productivity assistant, or team collaboration tool, you'll learn the exact patterns used by successful Slack apps processing millions of requests daily.

Why Slack Integration Matters for ChatGPT Apps: Unlike standalone web applications, Slack integrations meet users where they work. Your ChatGPT app can respond to natural language commands, process team conversations, automate workflows, and deliver AI-powered insights without users leaving their communication hub. Integration reduces friction, increases adoption, and creates network effects as teams discover your app through organic usage.

MakeAIHQ's Approach: MakeAIHQ enables no-code Slack integration creation for ChatGPT apps, generating production-ready OAuth flows, slash command handlers, and Block Kit interfaces automatically. Our platform handles security, rate limiting, and webhook verification out of the box, letting you focus on your app's unique value proposition rather than integration plumbing.

Let's build a robust Slack integration that scales to millions of users while maintaining sub-200ms response times and 99.9% uptime.


Slack App Configuration & OAuth Setup

Before writing code, configure your Slack app through the App Manifest and OAuth settings. The app manifest defines your app's capabilities, required scopes, and integration points in a declarative YAML format.

Slack App Manifest Configuration

Create a comprehensive app manifest that declares all features your ChatGPT integration needs:

display_information:
  name: ChatGPT Assistant
  description: AI-powered assistant bringing ChatGPT's capabilities to your workspace
  background_color: "#0A0E27"
  long_description: "Transform your Slack workspace with ChatGPT-powered assistance. Get instant answers, automate workflows, summarize conversations, and boost team productivity with natural language commands."

features:
  app_home:
    home_tab_enabled: true
    messages_tab_enabled: true
    messages_tab_read_only_enabled: false
  bot_user:
    display_name: ChatGPT Assistant
    always_online: true
  slash_commands:
    - command: /chatgpt
      url: https://your-api.makeaihq.com/slack/commands
      description: Ask ChatGPT anything
      usage_hint: "[your question]"
      should_escape: false
    - command: /summarize
      url: https://your-api.makeaihq.com/slack/commands
      description: Summarize thread or channel
      usage_hint: "[thread_ts or channel]"
      should_escape: false
  shortcuts:
    - name: Ask ChatGPT
      type: message
      callback_id: ask_chatgpt_shortcut
      description: Get AI insights on any message

oauth_config:
  redirect_urls:
    - https://your-api.makeaihq.com/slack/oauth/redirect
    - https://chatgpt.com/connector_platform_oauth_redirect
  scopes:
    bot:
      - channels:history
      - channels:read
      - chat:write
      - commands
      - groups:history
      - groups:read
      - im:history
      - im:read
      - im:write
      - users:read
      - users:read.email
      - app_mentions:read

settings:
  event_subscriptions:
    request_url: https://your-api.makeaihq.com/slack/events
    bot_events:
      - app_mention
      - message.channels
      - message.groups
      - message.im
  interactivity:
    is_enabled: true
    request_url: https://your-api.makeaihq.com/slack/interactive
  org_deploy_enabled: false
  socket_mode_enabled: true
  token_rotation_enabled: true

This manifest declares OAuth scopes following the principle of least privilege—requesting only permissions essential for your integration's functionality.

OAuth 2.1 PKCE Implementation

Implement secure OAuth authentication with PKCE (Proof Key for Code Exchange) to protect against authorization code interception attacks:

import crypto from 'crypto';
import { WebClient } from '@slack/web-api';

interface OAuthState {
  codeVerifier: string;
  codeChallenge: string;
  state: string;
  teamId?: string;
  userId?: string;
}

class SlackOAuthHandler {
  private clientId: string;
  private clientSecret: string;
  private redirectUri: string;
  private stateStore: Map<string, OAuthState>;

  constructor(config: { clientId: string; clientSecret: string; redirectUri: string }) {
    this.clientId = config.clientId;
    this.clientSecret = config.clientSecret;
    this.redirectUri = config.redirectUri;
    this.stateStore = new Map();
  }

  /**
   * Generate PKCE code verifier and challenge
   * Code verifier: 43-128 character random string
   * Code challenge: Base64-URL-encoded SHA256 hash of verifier
   */
  private generatePKCE(): { codeVerifier: string; codeChallenge: string } {
    const codeVerifier = crypto.randomBytes(64).toString('base64url');
    const codeChallenge = crypto
      .createHash('sha256')
      .update(codeVerifier)
      .digest('base64url');

    return { codeVerifier, codeChallenge };
  }

  /**
   * Generate authorization URL for Slack OAuth flow
   * Returns URL that redirects user to Slack's consent screen
   */
  generateAuthUrl(teamId?: string, userId?: string): string {
    const { codeVerifier, codeChallenge } = this.generatePKCE();
    const state = crypto.randomBytes(32).toString('base64url');

    // Store PKCE parameters for verification during callback
    this.stateStore.set(state, {
      codeVerifier,
      codeChallenge,
      state,
      teamId,
      userId,
    });

    // Auto-expire state after 10 minutes
    setTimeout(() => this.stateStore.delete(state), 10 * 60 * 1000);

    const params = new URLSearchParams({
      client_id: this.clientId,
      scope: 'channels:history,channels:read,chat:write,commands,users:read',
      redirect_uri: this.redirectUri,
      state,
      code_challenge: codeChallenge,
      code_challenge_method: 'S256',
    });

    return `https://slack.com/oauth/v2/authorize?${params.toString()}`;
  }

  /**
   * Handle OAuth callback and exchange authorization code for access token
   * Validates state parameter and PKCE code verifier
   */
  async handleCallback(code: string, state: string): Promise<{
    accessToken: string;
    botUserId: string;
    teamId: string;
    teamName: string;
    authedUserId: string;
  }> {
    // Validate state parameter to prevent CSRF attacks
    const storedState = this.stateStore.get(state);
    if (!storedState) {
      throw new Error('Invalid or expired state parameter');
    }

    try {
      // Exchange authorization code for access token
      const client = new WebClient();
      const response = await client.oauth.v2.access({
        client_id: this.clientId,
        client_secret: this.clientSecret,
        code,
        redirect_uri: this.redirectUri,
        code_verifier: storedState.codeVerifier,
      });

      if (!response.ok || !response.access_token) {
        throw new Error('OAuth exchange failed');
      }

      // Clean up state after successful exchange
      this.stateStore.delete(state);

      return {
        accessToken: response.access_token,
        botUserId: response.bot_user_id || '',
        teamId: response.team?.id || '',
        teamName: response.team?.name || '',
        authedUserId: response.authed_user?.id || '',
      };
    } catch (error) {
      this.stateStore.delete(state);
      throw new Error(`OAuth callback failed: ${error.message}`);
    }
  }

  /**
   * Refresh access token using refresh token
   * Slack rotates tokens automatically when token_rotation_enabled: true
   */
  async refreshToken(refreshToken: string): Promise<string> {
    const client = new WebClient();
    const response = await client.oauth.v2.access({
      client_id: this.clientId,
      client_secret: this.clientSecret,
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
    });

    if (!response.ok || !response.access_token) {
      throw new Error('Token refresh failed');
    }

    return response.access_token;
  }
}

export { SlackOAuthHandler, OAuthState };

This OAuth implementation follows OAuth 2.1 best practices with PKCE, state validation, and automatic token refresh. Store access tokens securely in your database encrypted at rest.


Slash Commands Implementation

Slash commands provide the primary interface for users to invoke your ChatGPT integration. When a user types /chatgpt [query], Slack sends an HTTP POST to your webhook endpoint with the command payload.

Slash Command Handler with MCP Tool Mapping

Build a robust slash command handler that maps Slack commands to MCP tools in your ChatGPT app:

import { WebClient } from '@slack/web-api';
import { MCPClient } from '@modelcontextprotocol/sdk';

interface SlackCommand {
  token: string;
  team_id: string;
  team_domain: string;
  channel_id: string;
  channel_name: string;
  user_id: string;
  user_name: string;
  command: string;
  text: string;
  api_app_id: string;
  response_url: string;
  trigger_id: string;
}

interface MCPToolResponse {
  content: string;
  structuredContent?: any;
  _meta?: any;
}

class SlackCommandHandler {
  private slackClient: WebClient;
  private mcpClient: MCPClient;
  private verificationToken: string;

  constructor(token: string, verificationToken: string, mcpEndpoint: string) {
    this.slackClient = new WebClient(token);
    this.verificationToken = verificationToken;
    this.mcpClient = new MCPClient(mcpEndpoint);
  }

  /**
   * Verify Slack request authenticity using verification token
   * In production, use request signature verification instead
   */
  private verifyRequest(command: SlackCommand): boolean {
    return command.token === this.verificationToken;
  }

  /**
   * Handle /chatgpt command
   * Responds with immediate acknowledgment, then processes asynchronously
   */
  async handleChatGPTCommand(command: SlackCommand): Promise<any> {
    if (!this.verifyRequest(command)) {
      return { response_type: 'ephemeral', text: 'Invalid request signature' };
    }

    // Send immediate loading response (must respond within 3 seconds)
    const loadingResponse = {
      response_type: 'in_channel',
      text: `Processing your question: "${command.text}"`,
      blocks: [
        {
          type: 'section',
          text: {
            type: 'mrkdwn',
            text: `:hourglass_flowing_sand: *Processing your question...*\n> ${command.text}`,
          },
        },
      ],
    };

    // Process command asynchronously using response_url
    this.processCommandAsync(command).catch(error => {
      console.error('Async command processing failed:', error);
    });

    return loadingResponse;
  }

  /**
   * Process command asynchronously and update original message
   * Uses response_url for delayed responses beyond 3-second window
   */
  private async processCommandAsync(command: SlackCommand): Promise<void> {
    try {
      // Call MCP tool with user's query
      const mcpResponse = await this.mcpClient.callTool('chatgpt_query', {
        query: command.text,
        context: {
          channel: command.channel_id,
          user: command.user_id,
          team: command.team_id,
        },
      });

      // Format response with Block Kit
      const responseBlocks = this.formatMCPResponse(mcpResponse, command);

      // Update original message via response_url
      await fetch(command.response_url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          replace_original: true,
          response_type: 'in_channel',
          blocks: responseBlocks,
        }),
      });
    } catch (error) {
      // Send error response
      await fetch(command.response_url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          replace_original: true,
          response_type: 'ephemeral',
          text: `Error processing request: ${error.message}`,
        }),
      });
    }
  }

  /**
   * Format MCP tool response as Slack Block Kit blocks
   * Converts ChatGPT responses into rich, interactive Slack messages
   */
  private formatMCPResponse(response: MCPToolResponse, command: SlackCommand): any[] {
    const blocks: any[] = [
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `*Question:* ${command.text}`,
        },
      },
      {
        type: 'divider',
      },
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: response.content || 'No response generated',
        },
      },
    ];

    // Add interactive buttons if structured content available
    if (response.structuredContent?.actions) {
      blocks.push({
        type: 'actions',
        elements: response.structuredContent.actions.map((action: any) => ({
          type: 'button',
          text: {
            type: 'plain_text',
            text: action.label,
          },
          action_id: action.id,
          value: JSON.stringify(action.value),
        })),
      });
    }

    // Add metadata footer
    blocks.push({
      type: 'context',
      elements: [
        {
          type: 'mrkdwn',
          text: `Powered by ChatGPT via MakeAIHQ | <${command.response_url}|Refresh>`,
        },
      ],
    });

    return blocks;
  }

  /**
   * Handle /summarize command for thread/channel summarization
   */
  async handleSummarizeCommand(command: SlackCommand): Promise<any> {
    if (!this.verifyRequest(command)) {
      return { response_type: 'ephemeral', text: 'Invalid request signature' };
    }

    // Fetch conversation history
    const messages = await this.fetchConversationHistory(command.channel_id);

    // Send to MCP summarization tool
    const summary = await this.mcpClient.callTool('summarize_conversation', {
      messages: messages.map(msg => ({
        user: msg.user,
        text: msg.text,
        timestamp: msg.ts,
      })),
    });

    return {
      response_type: 'in_channel',
      blocks: [
        {
          type: 'section',
          text: {
            type: 'mrkdwn',
            text: `*Channel Summary*\n${summary.content}`,
          },
        },
      ],
    };
  }

  private async fetchConversationHistory(channelId: string): Promise<any[]> {
    const result = await this.slackClient.conversations.history({
      channel: channelId,
      limit: 100,
    });
    return result.messages || [];
  }
}

export { SlackCommandHandler, SlackCommand };

This implementation handles Slack's 3-second response deadline by sending immediate acknowledgments and processing requests asynchronously via response_url. Learn more about MCP Tool Handler Best Practices for optimal tool design.


Interactive Components

Block Kit interactive components enable rich user interactions beyond simple slash commands. Users can click buttons, select from dropdowns, submit forms, and interact with your ChatGPT app through visual interfaces.

Block Kit Interactive Component Handler

Implement handlers for buttons, select menus, modals, and other interactive elements:

import { WebClient } from '@slack/web-api';

interface InteractivePayload {
  type: 'block_actions' | 'view_submission' | 'view_closed' | 'shortcut';
  user: { id: string; username: string; team_id: string };
  api_app_id: string;
  token: string;
  trigger_id?: string;
  response_url?: string;
  actions?: Array<{
    action_id: string;
    block_id: string;
    value?: string;
    selected_option?: any;
    type: string;
  }>;
  view?: any;
  message?: any;
  channel?: { id: string; name: string };
}

class SlackInteractiveHandler {
  private slackClient: WebClient;
  private mcpClient: any;

  constructor(token: string, mcpClient: any) {
    this.slackClient = new WebClient(token);
    this.mcpClient = mcpClient;
  }

  /**
   * Route interactive payloads to appropriate handlers
   */
  async handleInteraction(payload: InteractivePayload): Promise<any> {
    switch (payload.type) {
      case 'block_actions':
        return this.handleBlockActions(payload);
      case 'view_submission':
        return this.handleViewSubmission(payload);
      case 'shortcut':
        return this.handleShortcut(payload);
      default:
        console.warn('Unknown interaction type:', payload.type);
        return { statusCode: 200 };
    }
  }

  /**
   * Handle button clicks and select menu interactions
   */
  private async handleBlockActions(payload: InteractivePayload): Promise<any> {
    const action = payload.actions?.[0];
    if (!action) return { statusCode: 200 };

    switch (action.action_id) {
      case 'refine_answer':
        return this.handleRefineAnswer(payload, action);
      case 'ask_followup':
        return this.handleFollowUpQuestion(payload);
      case 'export_conversation':
        return this.handleExportConversation(payload);
      default:
        console.warn('Unknown action_id:', action.action_id);
        return { statusCode: 200 };
    }
  }

  /**
   * Handle "Refine Answer" button click
   * Opens modal for user to provide refinement instructions
   */
  private async handleRefineAnswer(
    payload: InteractivePayload,
    action: any
  ): Promise<any> {
    const modalView = {
      type: 'modal',
      callback_id: 'refine_answer_modal',
      title: {
        type: 'plain_text',
        text: 'Refine Answer',
      },
      submit: {
        type: 'plain_text',
        text: 'Refine',
      },
      close: {
        type: 'plain_text',
        text: 'Cancel',
      },
      private_metadata: JSON.stringify({
        originalQuery: action.value,
        channelId: payload.channel?.id,
        messageTs: payload.message?.ts,
      }),
      blocks: [
        {
          type: 'input',
          block_id: 'refinement_input',
          label: {
            type: 'plain_text',
            text: 'How should I refine this answer?',
          },
          element: {
            type: 'plain_text_input',
            action_id: 'refinement_text',
            multiline: true,
            placeholder: {
              type: 'plain_text',
              text: 'E.g., "Make it shorter", "Add more examples", "Focus on X"',
            },
          },
        },
      ],
    };

    await this.slackClient.views.open({
      trigger_id: payload.trigger_id!,
      view: modalView,
    });

    return { statusCode: 200 };
  }

  /**
   * Handle modal submission
   * Process refined query through MCP and update original message
   */
  private async handleViewSubmission(payload: InteractivePayload): Promise<any> {
    if (payload.view?.callback_id !== 'refine_answer_modal') {
      return { statusCode: 200 };
    }

    const metadata = JSON.parse(payload.view.private_metadata);
    const refinementText =
      payload.view.state.values.refinement_input.refinement_text.value;

    // Call MCP tool with refinement request
    const refinedResponse = await this.mcpClient.callTool('refine_answer', {
      originalQuery: metadata.originalQuery,
      refinement: refinementText,
    });

    // Update original message
    await this.slackClient.chat.update({
      channel: metadata.channelId,
      ts: metadata.messageTs,
      blocks: [
        {
          type: 'section',
          text: {
            type: 'mrkdwn',
            text: `*Refined Answer*\n${refinedResponse.content}`,
          },
        },
        {
          type: 'context',
          elements: [
            {
              type: 'mrkdwn',
              text: `Refined based on: "${refinementText}"`,
            },
          ],
        },
      ],
    });

    return { response_action: 'clear' };
  }

  /**
   * Handle message shortcut (right-click on message → "Ask ChatGPT")
   */
  private async handleShortcut(payload: InteractivePayload): Promise<any> {
    // Implement message shortcut logic here
    return { statusCode: 200 };
  }

  private async handleFollowUpQuestion(payload: InteractivePayload): Promise<any> {
    // Open modal for follow-up question
    return { statusCode: 200 };
  }

  private async handleExportConversation(payload: InteractivePayload): Promise<any> {
    // Export conversation thread as PDF/Markdown
    return { statusCode: 200 };
  }
}

export { SlackInteractiveHandler, InteractivePayload };

Block Kit enables sophisticated interfaces like multi-step wizards, data tables, and real-time dashboards. Explore the Block Kit Builder for visual design tools and component documentation.


Event Subscriptions

Event subscriptions enable your ChatGPT app to react to workspace activity in real-time. When a user mentions your bot, sends a direct message, or posts in a monitored channel, Slack sends event payloads to your webhook endpoint.

Event Subscription Webhook Handler

Build a webhook handler that processes Slack events and triggers appropriate MCP tools:

import crypto from 'crypto';
import { WebClient } from '@slack/web-api';

interface SlackEvent {
  token: string;
  team_id: string;
  api_app_id: string;
  event: {
    type: string;
    user?: string;
    text?: string;
    ts: string;
    channel?: string;
    channel_type?: string;
    thread_ts?: string;
    bot_id?: string;
  };
  type: 'url_verification' | 'event_callback';
  challenge?: string;
  event_id: string;
  event_time: number;
}

class SlackEventHandler {
  private slackClient: WebClient;
  private mcpClient: any;
  private signingSecret: string;
  private processedEvents: Set<string>;

  constructor(token: string, signingSecret: string, mcpClient: any) {
    this.slackClient = new WebClient(token);
    this.signingSecret = signingSecret;
    this.mcpClient = mcpClient;
    this.processedEvents = new Set();

    // Clear processed events cache every hour to prevent memory leaks
    setInterval(() => this.processedEvents.clear(), 60 * 60 * 1000);
  }

  /**
   * Verify Slack request signature to prevent spoofing
   * Critical for production security
   */
  verifySignature(
    signature: string,
    timestamp: string,
    body: string
  ): boolean {
    // Reject requests older than 5 minutes to prevent replay attacks
    const requestTime = parseInt(timestamp, 10);
    const currentTime = Math.floor(Date.now() / 1000);
    if (Math.abs(currentTime - requestTime) > 60 * 5) {
      return false;
    }

    // Compute expected signature
    const sigBasestring = `v0:${timestamp}:${body}`;
    const expectedSignature =
      'v0=' +
      crypto
        .createHmac('sha256', this.signingSecret)
        .update(sigBasestring)
        .digest('hex');

    // Constant-time comparison to prevent timing attacks
    return crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expectedSignature)
    );
  }

  /**
   * Handle incoming Slack events
   * Responds to url_verification challenge during initial setup
   */
  async handleEvent(
    event: SlackEvent,
    signature: string,
    timestamp: string,
    rawBody: string
  ): Promise<any> {
    // Verify request authenticity
    if (!this.verifySignature(signature, timestamp, rawBody)) {
      throw new Error('Invalid request signature');
    }

    // Handle URL verification challenge
    if (event.type === 'url_verification') {
      return { challenge: event.challenge };
    }

    // Prevent duplicate event processing (Slack retries on failure)
    if (this.processedEvents.has(event.event_id)) {
      console.log('Duplicate event ignored:', event.event_id);
      return { statusCode: 200 };
    }
    this.processedEvents.add(event.event_id);

    // Route to appropriate event handler
    await this.routeEvent(event);

    // Always respond with 200 within 3 seconds
    return { statusCode: 200 };
  }

  /**
   * Route events to specialized handlers based on event type
   */
  private async routeEvent(event: SlackEvent): Promise<void> {
    switch (event.event.type) {
      case 'app_mention':
        await this.handleAppMention(event);
        break;
      case 'message':
        await this.handleMessage(event);
        break;
      default:
        console.log('Unhandled event type:', event.event.type);
    }
  }

  /**
   * Handle @bot mentions in channels
   * Processes question and responds in thread
   */
  private async handleAppMention(event: SlackEvent): Promise<void> {
    const { text, channel, user, ts, thread_ts } = event.event;

    // Extract question (remove bot mention)
    const question = text?.replace(/<@\w+>/g, '').trim() || '';

    try {
      // Call MCP tool to generate response
      const response = await this.mcpClient.callTool('chatgpt_query', {
        query: question,
        context: {
          channel,
          user,
          thread_ts: thread_ts || ts,
        },
      });

      // Reply in thread to keep channel organized
      await this.slackClient.chat.postMessage({
        channel: channel!,
        thread_ts: thread_ts || ts,
        text: response.content,
        blocks: [
          {
            type: 'section',
            text: {
              type: 'mrkdwn',
              text: response.content,
            },
          },
        ],
      });
    } catch (error) {
      console.error('Error handling app mention:', error);
      await this.slackClient.chat.postMessage({
        channel: channel!,
        thread_ts: thread_ts || ts,
        text: `Sorry, I encountered an error: ${error.message}`,
      });
    }
  }

  /**
   * Handle direct messages to bot
   */
  private async handleMessage(event: SlackEvent): Promise<void> {
    // Ignore bot messages to prevent loops
    if (event.event.bot_id) return;

    // Only process DMs (channel_type === 'im')
    if (event.event.channel_type !== 'im') return;

    const { text, channel, user, ts } = event.event;

    // Similar processing to app mentions...
    // Implementation omitted for brevity
  }
}

export { SlackEventHandler, SlackEvent };

Event subscriptions enable powerful automation like auto-summarization, sentiment analysis, and proactive assistance. Review Webhook Security Best Practices for production hardening.


Production Deployment

Deploying a Slack integration to production requires careful consideration of connection modes, error handling, rate limiting, and monitoring. Choose between Socket Mode (WebSocket connections) and HTTP mode (webhooks) based on your infrastructure.

Rate Limiter Middleware

Implement rate limiting to comply with Slack API tier limits (1 request/second for most methods, higher for chat.postMessage):

interface RateLimitConfig {
  maxRequests: number;
  windowMs: number;
  keyGenerator: (req: any) => string;
}

class RateLimiter {
  private requests: Map<string, number[]>;
  private config: RateLimitConfig;

  constructor(config: RateLimitConfig) {
    this.requests = new Map();
    this.config = config;

    // Clean up expired entries every minute
    setInterval(() => this.cleanup(), 60 * 1000);
  }

  /**
   * Check if request should be allowed
   * Returns true if under limit, false if rate limited
   */
  async checkLimit(req: any): Promise<boolean> {
    const key = this.config.keyGenerator(req);
    const now = Date.now();
    const windowStart = now - this.config.windowMs;

    // Get request timestamps for this key
    const timestamps = this.requests.get(key) || [];

    // Remove timestamps outside current window
    const validTimestamps = timestamps.filter(ts => ts > windowStart);

    // Check if under limit
    if (validTimestamps.length >= this.config.maxRequests) {
      return false;
    }

    // Add current request timestamp
    validTimestamps.push(now);
    this.requests.set(key, validTimestamps);

    return true;
  }

  private cleanup(): void {
    const now = Date.now();
    for (const [key, timestamps] of this.requests.entries()) {
      const validTimestamps = timestamps.filter(
        ts => ts > now - this.config.windowMs
      );
      if (validTimestamps.length === 0) {
        this.requests.delete(key);
      } else {
        this.requests.set(key, validTimestamps);
      }
    }
  }
}

// Usage example
const rateLimiter = new RateLimiter({
  maxRequests: 60,
  windowMs: 60 * 1000, // 1 minute
  keyGenerator: (req) => req.team_id || req.user_id,
});

export { RateLimiter };

Learn advanced rate limiting strategies in our API Rate Limiting Strategies guide.

Socket Mode Connection Manager

For Socket Mode deployments (no public endpoints needed), implement a connection manager with automatic reconnection:

import { SocketModeClient } from '@slack/socket-mode';
import { WebClient } from '@slack/web-api';

class SocketModeManager {
  private socketClient: SocketModeClient;
  private webClient: WebClient;
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 10;

  constructor(appToken: string, botToken: string) {
    this.socketClient = new SocketModeClient({ appToken });
    this.webClient = new WebClient(botToken);

    this.setupEventHandlers();
  }

  private setupEventHandlers(): void {
    // Handle slash commands
    this.socketClient.on('slash_commands', async ({ body, ack }) => {
      await ack();
      // Process command asynchronously
    });

    // Handle interactive events
    this.socketClient.on('interactive', async ({ body, ack }) => {
      await ack();
      // Process interaction
    });

    // Handle connection events
    this.socketClient.on('connected', () => {
      console.log('Socket Mode connected');
      this.reconnectAttempts = 0;
    });

    this.socketClient.on('disconnected', () => {
      console.warn('Socket Mode disconnected');
      this.handleReconnect();
    });

    this.socketClient.on('error', (error) => {
      console.error('Socket Mode error:', error);
    });
  }

  private async handleReconnect(): Promise<void> {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.error('Max reconnection attempts reached');
      return;
    }

    this.reconnectAttempts++;
    const backoffMs = Math.min(1000 * 2 ** this.reconnectAttempts, 30000);

    console.log(`Reconnecting in ${backoffMs}ms (attempt ${this.reconnectAttempts})`);
    setTimeout(() => this.start(), backoffMs);
  }

  async start(): Promise<void> {
    await this.socketClient.start();
  }

  async stop(): Promise<void> {
    await this.socketClient.disconnect();
  }
}

export { SocketModeManager };

Error Handler with Retry Logic

Implement robust error handling with exponential backoff for transient failures:

interface RetryConfig {
  maxRetries: number;
  baseDelayMs: number;
  maxDelayMs: number;
}

class ErrorHandler {
  private config: RetryConfig;

  constructor(config: RetryConfig = {
    maxRetries: 3,
    baseDelayMs: 1000,
    maxDelayMs: 10000,
  }) {
    this.config = config;
  }

  /**
   * Execute function with exponential backoff retry
   */
  async withRetry<T>(
    fn: () => Promise<T>,
    context?: string
  ): Promise<T> {
    let lastError: Error | null = null;

    for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
      try {
        return await fn();
      } catch (error: any) {
        lastError = error;

        // Don't retry on client errors (4xx)
        if (error.statusCode >= 400 && error.statusCode < 500) {
          throw error;
        }

        if (attempt < this.config.maxRetries) {
          const delayMs = Math.min(
            this.config.baseDelayMs * 2 ** attempt,
            this.config.maxDelayMs
          );
          console.warn(
            `Retry attempt ${attempt + 1}/${this.config.maxRetries} ` +
            `after ${delayMs}ms. Error: ${error.message}`,
            { context }
          );
          await this.sleep(delayMs);
        }
      }
    }

    throw lastError;
  }

  private sleep(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

export { ErrorHandler };

Monitoring Dashboard with Prometheus Metrics

Export metrics for observability using Prometheus client:

import { Counter, Histogram, Registry } from 'prom-client';

class SlackMetrics {
  private registry: Registry;
  private commandCounter: Counter;
  private eventCounter: Counter;
  private latencyHistogram: Histogram;
  private errorCounter: Counter;

  constructor() {
    this.registry = new Registry();

    this.commandCounter = new Counter({
      name: 'slack_commands_total',
      help: 'Total slash commands received',
      labelNames: ['command', 'team_id'],
      registers: [this.registry],
    });

    this.eventCounter = new Counter({
      name: 'slack_events_total',
      help: 'Total events received',
      labelNames: ['event_type', 'team_id'],
      registers: [this.registry],
    });

    this.latencyHistogram = new Histogram({
      name: 'slack_request_duration_seconds',
      help: 'Request latency in seconds',
      labelNames: ['type', 'status'],
      buckets: [0.1, 0.5, 1, 2, 5],
      registers: [this.registry],
    });

    this.errorCounter = new Counter({
      name: 'slack_errors_total',
      help: 'Total errors encountered',
      labelNames: ['type', 'error_code'],
      registers: [this.registry],
    });
  }

  recordCommand(command: string, teamId: string): void {
    this.commandCounter.inc({ command, team_id: teamId });
  }

  recordEvent(eventType: string, teamId: string): void {
    this.eventCounter.inc({ event_type: eventType, team_id: teamId });
  }

  recordLatency(type: string, durationSeconds: number, status: string): void {
    this.latencyHistogram.observe({ type, status }, durationSeconds);
  }

  recordError(type: string, errorCode: string): void {
    this.errorCounter.inc({ type, error_code: errorCode });
  }

  async getMetrics(): Promise<string> {
    return this.registry.metrics();
  }
}

export { SlackMetrics };

Integrate with monitoring tools like Grafana for real-time dashboards. See Production Monitoring Dashboards for complete setup guides.


Conclusion: Building Production-Ready Slack Integrations

You've learned how to build enterprise-grade Slack integrations for ChatGPT apps, covering OAuth 2.1 with PKCE, slash commands, interactive components, event subscriptions, rate limiting, error handling, and monitoring. These patterns power integrations processing millions of requests daily at sub-200ms latency.

Key Takeaways:

  • OAuth 2.1 PKCE flow protects against authorization code interception
  • Slash commands require <3s responses; use response_url for async processing
  • Block Kit interactive components enable rich user experiences
  • Event signature verification prevents spoofing attacks
  • Rate limiting, retries, and monitoring ensure production reliability

Next Steps: Ready to build your Slack integration without writing thousands of lines of code? MakeAIHQ's Instant App Wizard generates production-ready Slack integrations with OAuth, slash commands, and interactive components in under 5 minutes. Our no-code platform handles security, rate limiting, and deployment automatically.

Start building your Slack ChatGPT integration →


Related Resources

Internal Links

External Authoritative Links


Author: MakeAIHQ Engineering Team Published: December 25, 2026 Last Updated: December 25, 2026

Building the future of no-code ChatGPT app development, one integration at a time.