← Back to DevBytes

How to Create a Slack Bot with Python and Flask

What is a Slack Bot?

A Slack bot is an automated software program that interacts with users inside a Slack workspace. It can respond to slash commands, listen for specific keywords in conversations, send direct messages, post updates to channels, and even integrate with external APIs. Bots appear as special user-like entities with names, profile pictures, and a distinctive "BOT" badge, making them easily identifiable in the sidebar.

Unlike regular Slack integrations that simply push notifications, a well-built bot can carry on meaningful two-way conversations, process natural language, and perform complex workflows — from creating Jira tickets to querying databases — all without leaving the Slack interface.

Why Build a Slack Bot with Python and Flask?

Python's readability and rich ecosystem make it an ideal choice for bot development. Flask, a lightweight WSGI web framework, is perfect for exposing the HTTP endpoints that Slack requires for real-time communication. Together they offer:

Prerequisites

Before writing any code, make sure you have:

Step 1: Create Your Slack App

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Head over to api.slack.com/apps and click "Create New App". Choose "From scratch", give your bot a name (e.g., "WeatherBot"), and select your target workspace. This generates the app record and brings you to the configuration dashboard.

Configure Bot Token Scopes

Navigate to "OAuth & Permissions" in the sidebar. Under "Bot Token Scopes", add the scopes your bot needs. For a basic interactive bot, start with:

After saving, scroll up and click "Install to Workspace". Authorize the app, and you'll receive a Bot Token (starting with xoxb-). Copy this token — you'll need it shortly.

Enable Event Subscriptions

Go to "Event Subscriptions" and toggle "Enable Events". For now, leave the Request URL blank (we'll fill it after setting up ngrok). Subscribe to the app_mention event so your bot receives a payload whenever someone mentions it in a channel.

Create a Slash Command

Under "Slash Commands", click "Create New Command". Fill in:

Save the command. Slack will later POST to your Flask endpoint whenever a user types /weather London.

Step 2: Set Up the Python Project

Create a project directory and a virtual environment:

mkdir slack-weather-bot
cd slack-weather-bot
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

Install the required packages:

pip install flask slack_sdk python-dotenv requests

Create a .env file to store secrets:

SLACK_BOT_TOKEN=xoxb-your-bot-token-here
SLACK_SIGNING_SECRET=your-signing-secret-here

The Signing Secret is found under "Basic Information" in your Slack app dashboard. It verifies that incoming requests genuinely originate from Slack.

Step 3: Build the Flask Application

Create a file called app.py. This will be the heart of your bot. Below is the complete starter code with detailed explanations.

# app.py
import os
import json
import requests
from flask import Flask, request, jsonify, Response
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
from slack_sdk.signature import SignatureVerifier
from dotenv import load_dotenv
import time

load_dotenv()

app = Flask(__name__)

# Initialize the Slack Web client for API calls
client = WebClient(token=os.environ.get("SLACK_BOT_TOKEN"))

# Signature verifier ensures requests actually come from Slack
signature_verifier = SignatureVerifier(
    signing_secret=os.environ.get("SLACK_SIGNING_SECRET")
)

# --------------------- SLASH COMMAND HANDLER ---------------------
@app.route("/slack/commands", methods=["POST"])
def handle_slash_command():
    """
    Slack sends a POST with form-encoded data when a user invokes
    a slash command. We parse it, verify the signature, and respond
    with either an immediate acknowledgment or a deferred response.
    """
    # Verify that the request came from Slack
    if not verify_slack_request():
        return Response("Invalid signature", status=401)

    # Parse form data
    command = request.form.get("command", "")
    text = request.form.get("text", "")       # arguments after the command
    user_id = request.form.get("user_id", "")
    channel_id = request.form.get("channel_id", "")
    response_url = request.form.get("response_url", "")

    # Route based on command name
    if command == "/weather":
        return handle_weather_command(text, user_id, channel_id, response_url)

    # Unknown command fallback
    return jsonify({"text": f"Unknown command: {command}"}), 200


# --------------------- EVENT SUBSCRIPTION HANDLER ---------------------
@app.route("/slack/events", methods=["POST"])
def handle_slack_events():
    """
    Slack sends event payloads as JSON. We must respond with a
    200 OK within 3 seconds, then process the event asynchronously
    if needed. URL verification requires echoing the challenge.
    """
    # URL verification handshake (happens when you save the URL)
    if request.json and request.json.get("type") == "url_verification":
        challenge = request.json.get("challenge")
        return jsonify({"challenge": challenge}), 200

    # Verify signature for all other event types
    if not verify_slack_request():
        return Response("Invalid signature", status=401)

    # Respond immediately to avoid timeout
    event_data = request.json

    # Process in a background-friendly way (here we use simple dispatch)
    if event_data and event_data.get("event"):
        event_type = event_data["event"]["type"]
        if event_type == "app_mention":
            handle_app_mention(event_data["event"])

    return "", 200


# --------------------- HELPER FUNCTIONS ---------------------
def verify_slack_request():
    """
    Uses Slack's signature verification to ensure authenticity.
    Returns True if valid, False otherwise.
    """
    body = request.get_data(as_text=True)
    timestamp = request.headers.get("X-Slack-Request-Timestamp", "")
    signature = request.headers.get("X-Slack-Signature", "")

    # Reject requests older than 5 minutes to prevent replay attacks
    if abs(time.time() - int(timestamp)) > 60 * 5:
        return False

    return signature_verifier.is_valid(
        body=body,
        timestamp=timestamp,
        signature=signature
    )


def handle_weather_command(city_text, user_id, channel_id, response_url):
    """
    Process /weather command. Responds immediately with an ephemeral
    message, then uses the response_url to post the actual weather
    data asynchronously for a richer experience.
    """
    if not city_text.strip():
        return jsonify({
            "response_type": "ephemeral",
            "text": "Please provide a city name. Usage: `/weather London`"
        })

    # Acknowledge immediately
    # We'll use response_url to send the actual weather later
    # For simplicity, we do a synchronous external API call here,
    # but in production you'd offload this to a background task queue.
    weather_info = fetch_weather(city_text.strip())

    # Post result to channel using response_url (allows delayed response)
    post_via_response_url(response_url, {
        "response_type": "in_channel",
        "text": weather_info
    })

    # Return empty 200 — we already sent the message via response_url
    return "", 200


def handle_app_mention(event):
    """
    When someone mentions the bot, respond in the same channel.
    """
    user = event.get("user", "")
    channel = event.get("channel", "")
    text = event.get("text", "")

    # Simple keyword detection
    if "weather" in text.lower():
        # Extract city from mention text, e.g., "@WeatherBot weather in Paris"
        parts = text.split("weather")
        city = parts[-1].strip().lstrip("in ").strip() if len(parts) > 1 else ""
        if city:
            weather_info = fetch_weather(city)
        else:
            weather_info = "Which city would you like weather for? :sunny:"
    elif "hello" in text.lower() or "hi" in text.lower():
        weather_info = f"Hello <@{user}>! :wave: I'm WeatherBot. Try asking me about weather in any city!"
    else:
        weather_info = (
            "I didn't quite understand that. Try:\n"
            "• `@WeatherBot weather in Tokyo`\n"
            "• `@WeatherBot hello`\n"
            "• `/weather London`"
        )

    try:
        client.chat_postMessage(
            channel=channel,
            text=weather_info,
            unfurl_links=False
        )
    except SlackApiError as e:
        print(f"Error posting message: {e.response['error']}")


def fetch_weather(city):
    """
    Mock weather function — replace with a real API like OpenWeatherMap.
    """
    # In production, use: requests.get(f"https://api.openweathermap.org/...")
    # For demo purposes, return a static response
    city_lower = city.lower()
    mock_data = {
        "london": "🌧️ London: 12°C, light rain, humidity 78%",
        "tokyo": "☀️ Tokyo: 22°C, clear sky, humidity 45%",
        "new york": "🌤️ New York: 18°C, partly cloudy, humidity 60%",
        "paris": "⛅ Paris: 15°C, scattered clouds, humidity 55%",
    }
    return mock_data.get(
        city_lower,
        f"🌍 Weather for '{city}' not available in demo mode. "
        f"Try: London, Tokyo, New York, or Paris."
    )


def post_via_response_url(response_url, payload):
    """
    Send a delayed or additional message using the response_url
    provided by Slack. This URL is valid for 30 minutes.
    """
    requests.post(
        response_url,
        headers={"Content-Type": "application/json"},
        data=json.dumps(payload)
    )


# --------------------- ENTRY POINT ---------------------
if __name__ == "__main__":
    app.run(host="0.0.0.0", port=3000, debug=True)

Understanding the Code Flow

Here's what happens when a user types /weather London:

For app_mention events, Slack sends a JSON payload to /slack/events. The handler first checks for a url_verification challenge (used when you first register the endpoint), then dispatches to handle_app_mention(), which uses the WebClient to post a threaded reply.

Step 4: Expose Your Local Server with Ngrok

Slack needs a publicly accessible HTTPS URL to reach your Flask app. During development, ngrok creates a secure tunnel to your localhost:

# In a new terminal window
ngrok http 3000

Ngrok will display a forwarding URL like https://abc123.ngrok.io. Copy this URL.

Now go back to your Slack app dashboard:

Save both. If Slack shows a green verification checkmark, you're connected.

Step 5: Run and Test Your Bot

Start the Flask server:

python app.py

In your Slack workspace, test the following:

Check your Flask terminal for any printed errors and the incoming request logs. Debug mode auto-reloads on code changes, so iteration is fast.

Step 6: Add Rich Formatting with Block Kit

Plain text responses work, but Block Kit lets you build richly structured messages with sections, dividers, buttons, and images. Here's how to upgrade the weather response:

def build_weather_blocks(city, temperature, condition, humidity):
    """
    Construct a Block Kit message for a weather report.
    """
    blocks = [
        {
            "type": "header",
            "text": {
                "type": "plain_text",
                "text": f"🌤️ Weather for {city}",
                "emoji": True
            }
        },
        {
            "type": "divider"
        },
        {
            "type": "section",
            "fields": [
                {
                    "type": "mrkdwn",
                    "text": f"*Temperature:*\n{temperature}"
                },
                {
                    "type": "mrkdwn",
                    "text": f"*Condition:*\n{condition}"
                },
                {
                    "type": "mrkdwn",
                    "text": f"*Humidity:*\n{humidity}%"
                }
            ]
        },
        {
            "type": "actions",
            "elements": [
                {
                    "type": "button",
                    "text": {
                        "type": "plain_text",
                        "text": "Refresh",
                        "emoji": True
                    },
                    "value": f"refresh_{city}",
                    "action_id": "weather_refresh"
                },
                {
                    "type": "button",
                    "text": {
                        "type": "plain_text",
                        "text": "Share",
                        "emoji": True
                    },
                    "value": f"share_{city}",
                    "action_id": "weather_share"
                }
            ]
        }
    ]
    return blocks


# Updated fetch_weather using blocks
def fetch_weather_with_blocks(city):
    """
    Fetch weather and return Block Kit payload.
    """
    # ... API call logic here ...
    blocks = build_weather_blocks(city, "18°C", "Partly Cloudy", "65")
    return blocks


# Updated slash command handler
def handle_weather_command(city_text, user_id, channel_id, response_url):
    if not city_text.strip():
        return jsonify({
            "response_type": "ephemeral",
            "blocks": [
                {
                    "type": "section",
                    "text": {
                        "type": "mrkdwn",
                        "text": "⚠️ Please provide a city name. Usage: `/weather London`"
                    }
                }
            ]
        })

    blocks = fetch_weather_with_blocks(city_text.strip())
    post_via_response_url(response_url, {
        "response_type": "in_channel",
        "blocks": blocks,
        "text": f"Weather update for {city_text}"  # fallback for notifications
    })
    return "", 200

Block Kit messages require a text fallback field for notification previews and accessibility. The blocks array defines the visual layout. Interactive elements like buttons require an additional endpoint to handle their action_id callbacks — Slack sends an interactive_message payload to your configured Interactions URL.

Step 7: Handle Interactive Messages (Buttons)

To respond to button clicks, add a new endpoint and register it in your Slack app dashboard under "Interactivity & Shortcuts":

@app.route("/slack/interactions", methods=["POST"])
def handle_interactions():
    """
    Handle button clicks, dropdown selections, and modal submissions.
    Slack sends the payload as form-encoded JSON in the 'payload' field.
    """
    # Verify signature
    if not verify_slack_request():
        return Response("Invalid signature", status=401)

    payload = json.loads(request.form.get("payload", "{}"))

    # Check if it's a block action
    if payload.get("type") == "block_actions":
        actions = payload.get("actions", [])
        for action in actions:
            action_id = action.get("action_id")
            if action_id == "weather_refresh":
                city = action.get("value", "").replace("refresh_", "")
                blocks = fetch_weather_with_blocks(city)
                # Update the original message (or post new)
                response_url = payload.get("response_url", "")
                post_via_response_url(response_url, {
                    "replace_original": "true",
                    "blocks": blocks,
                    "text": f"Refreshed weather for {city}"
                })
            elif action_id == "weather_share":
                city = action.get("value", "").replace("share_", "")
                # Post a new message to the channel
                client.chat_postMessage(
                    channel=payload["channel"]["id"],
                    text=f"Someone shared the weather for {city}!",
                    blocks=fetch_weather_with_blocks(city)
                )

    return "", 200

Set the Interactions Request URL in your Slack app to https://abc123.ngrok.io/slack/interactions. Now button clicks trigger your handler, enabling dynamic message updates without a full page reload on the Slack client side.

Best Practices for Slack Bot Development

1. Always Verify Signatures

Never skip the verify_slack_request() step. Without it, anyone could spoof Slack payloads to your endpoint, potentially posting unwanted messages to your workspace. The signing secret is the cryptographic guarantee that the request originated from Slack's servers.

2. Respond Within 3 Seconds (or Defer)

Slash commands and event subscriptions have a strict 3-second timeout. If your processing takes longer, Slack returns an error to the user. Use the response_url pattern shown above to acknowledge immediately and deliver results asynchronously. For even heavier workloads, queue the task with Celery or RQ and have the worker call the response_url upon completion.

3. Use Ephemeral Messages for Errors

When a user makes a mistake (like forgetting a required argument), respond with "response_type": "ephemeral". This shows the error only to that user, keeping channels clean. Reserve "in_channel" for results that benefit the whole team.

4. Handle Rate Limiting Gracefully

Slack's Web API enforces tiered rate limits. The slack_sdk WebClient includes built-in retry logic with exponential backoff, but you should still implement try/except blocks and log SlackApiError exceptions. For high-volume bots, track your request counts and implement queues to smooth out bursts.

5. Store Tokens Securely

Never hardcode tokens. Use environment variables (python-dotenv for local dev, platform secrets manager for production). Rotate tokens periodically. The Bot Token grants broad access — treat it like a password.

6. Implement Proper Logging

Add structured logging with timestamps, request IDs, and error details. This is invaluable when debugging why a slash command failed silently:

import logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s'
)
logger = logging.getLogger(__name__)

# Inside handlers:
logger.info(f"Processing /weather command from user={user_id} with text='{text}'")

7. Test Thoroughly with Multiple Workspaces

Install your app to a secondary test workspace in addition to your primary one. This lets you verify that scopes, event subscriptions, and command registrations work independently of your personal settings and channel memberships.

8. Handle Reconnects and Socket Mode

For bots that need to listen to many real-time events without exposing a public HTTP endpoint, Slack offers Socket Mode. Instead of HTTP POSTs, your bot opens a WebSocket connection to Slack and receives events over it. The slack_sdk supports this via SocketModeHandler:

from slack_sdk.socket_mode import SocketModeHandler
from slack_sdk.socket_mode.request import SocketModeRequest

def handle_socket_event(client: WebClient, req: SocketModeRequest):
    if req.type == "slash_commands":
        # Process slash command via socket
        pass
    elif req.type == "events_api":
        # Process event
        pass

handler = SocketModeHandler(app_token="xapp-...", web_client=client)
handler.connect()
handler.start()  # Blocks until interrupted

Socket Mode eliminates the need for ngrok during development and simplifies firewall configurations, though it requires an App-level token with connections:write scope.

9. Version Your API Endpoints

As your bot evolves, you may change the payload structure of your responses. Prefix your endpoints with version numbers (e.g., /v1/slack/commands) or include a version field in your responses so older clients don't break unexpectedly.

10. Document Your Bot Commands

Create a help command that lists all available interactions:

if command == "/weather-help":
    help_text = (
        "*Available commands:*\n"
        "• `/weather [city]` — Get current weather\n"
        "• `/weather-help` — Show this help message\n"
        "• `@WeatherBot weather in [city]` — Mention-based weather query\n"
        "• `@WeatherBot hello` — Greeting"
    )
    return jsonify({"text": help_text, "response_type": "ephemeral"})

Deployment Considerations

When you're ready to move beyond ngrok, deploy your Flask app to a production server. Popular options include:

Update your Slack app's Request URLs to point to the production domain, reinstall the app if scope changes occurred, and you're live.

Conclusion

Building a Slack bot with Python and Flask unlocks powerful automation directly within your team's communication hub. You've learned how to create a Slack app from scratch, configure OAuth scopes, handle slash commands and event subscriptions, verify request signatures, construct rich Block Kit messages, process interactive button clicks, and follow production-grade best practices. The combination of Flask's simplicity and the Slack SDK's comprehensive API coverage lets you build anything from a weather reporter to a full-fledged incident management bot. Start with the patterns shown here, iterate based on your team's feedback, and you'll have a bot that genuinely improves daily workflows.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles