Slack API Integration Guide: Web API, Events API & Webhooks (2026)

Slack has four API surfaces — Web API, Events API, Incoming Webhooks, and Socket Mode — and picking the wrong one is the most common reason Slack integrations need to be rebuilt. This guide explains what each surface does, which one your integration actually needs, and how to work with the key Web API endpoints (chat.postMessage, chat.update, conversations.list, users.lookupByEmail) with real code examples.

Quick answer: For most product integrations — sending notifications, DMs, interactive messages, slash commands — use the Web API with a bot token. Use the Events API when you need Slack to push events to your server in real time. Use Incoming Webhooks only for simple, one-way alerts to a fixed channel.

If you've ever searched "how to integrate with Slack," you've probably landed on a page that explains how to post a message using an Incoming Webhook — and wondered why there are three other APIs that seem to do something similar.

That confusion is real, and it costs engineering teams time. Slack has four distinct API surfaces: the Web API, the Events API, Incoming Webhooks, and Socket Mode. Each one exists for a different reason. Picking the wrong one means either building something that breaks when Slack's terms change, or over-engineering a simple notification system.

This guide cuts through that. By the end, you'll know exactly which Slack API surface your integration needs, how OAuth works, how the key endpoints behave, and how to handle slash commands and interactive messages. There's also a section on building via Knit, if you'd rather skip managing Slack auth and token lifecycle yourself and if you plan to add a ms teams integration later as it solves for  both in one go.

The Four Slack API Surfaces

1. Web API

The Slack Web API is a standard HTTPS REST API. You make requests to https://slack.com/api/{method}, pass a bot token in the Authorization header, and get JSON back. It is the foundation of most serious Slack integrations — over 100 methods are available covering messaging, user management, channels, files, and more.

Use it when you need to initiate actions from your server: send messages, look up users, list channels, update a message after it's been sent, or respond to interactions.

2. Events API

The Events API flips the direction. Instead of your server calling Slack, Slack calls your server via HTTP POST whenever something happens — a message is posted, a user joins a channel, a reaction is added, and so on. You register a public URL, Slack sends events to it, and you process them.

Use it when your integration needs to react to things happening in Slack: syncing messages to an external system, triggering workflows when users mention a keyword, or logging activity.

3. Incoming Webhooks

Incoming Webhooks are the simplest option. During app installation, Slack gives you a URL. You POST JSON to that URL and a message appears in a pre-configured channel. There's no OAuth flow to manage at runtime, no tokens to refresh — just one URL.

Use them when you want to push simple notifications from an external system into a single channel: CI/CD build alerts, server monitoring notifications, daily digest messages.

The constraint: each webhook is tied to one channel at install time. You can't dynamically choose where to send the message, and you can't read data or respond to events.

4. Socket Mode

Socket Mode lets your app receive events over a persistent WebSocket connection rather than an HTTP endpoint. This means Slack doesn't need to reach a public URL — useful during development, or when your app runs behind a firewall or in an environment where exposing a port isn't possible.

Use it for local development or for apps that live in environments without a public-facing URL. In production, the Events API is generally preferred.

What you want to do Recommended API surface
Send a message to a channel Web API — chat.postMessage
Send a direct message to a user Web API — chat.postMessage with user ID as channel
Update a message after it's sent Web API — chat.update
List channels or users Web API — conversations.list, users.list
React when a user posts a message Events API
Trigger a workflow when someone joins a channel Events API
Push a CI/CD alert to a fixed channel Incoming Webhooks
Handle a /command typed by a user Slash Commands (Web API + Events)
Build and test locally without a public URL Socket Mode
Let users click buttons in messages Web API + Interactive Components

Authentication: OAuth 2.0 for Slack Apps

Creating a Slack App

Start at api.slack.com/apps. Create a new app, either from scratch or from an app manifest. An app manifest is a YAML or JSON file that declares your app's permissions, event subscriptions, and slash commands — useful for version-controlling your app configuration.

Bot Tokens vs User Tokens

When your app is installed to a workspace, Slack issues two types of tokens:

  • Bot token (xoxb-...): Acts on behalf of your app's bot user. This is what most integrations use. The bot can only access channels it's been added to.
  • User token (xoxp-...): Acts on behalf of the user who installed the app. Has access to that user's data. Generally only needed if your integration requires user-level permissions (e.g., reading someone's private messages on their behalf).

For most integration use cases — sending notifications, managing channels, looking up users — a bot token is sufficient and the safer choice.

OAuth Scopes

Scopes define what your app can do. You declare required scopes when creating the app, and users see them listed when installing. Request only what you need — over-permissioned apps create friction at install time.

Common scopes for a messaging integration:

Scope What it allows
chat:write Post messages in channels the bot is a member of
chat:write.public Post to public channels without being a member
channels:read List public channels in the workspace
groups:read List private channels the bot has been added to
users:read View basic user information
users:read.email Look up users by email address
commands Add slash commands to the workspace
im:write Open direct message conversations

The OAuth Install Flow

  1. Direct the user to Slack's authorization URL with your client_id, requested scopes, and a redirect_uri.
  2. User approves the app and is redirected back to your redirect_uri with a temporary code.
  3. Your server exchanges the code for an access token via https://slack.com/api/oauth.v2.access.
  4. Store the access_token (and team_id) securely. This token doesn't expire — but users can revoke it, and you should handle token_revoked events.

Working with the Slack Web API

All Web API calls follow the same pattern:

POST https://slack.com/api/{method}
Authorization: Bearer xoxb-your-bot-token
Content-Type: application/json

Every response includes an "ok" boolean. If "ok": false, the "error" field tells you why.

{ "ok": false, "error": "channel_not_found" }

Always check ok before using the response body.

Sending Messages: chat.postMessage

The workhorse of most Slack integrations. Sends a message to a channel — or a DM when you pass a user ID as the channel.

POST https://slack.com/api/chat.postMessage
Authorization: Bearer xoxb-your-bot-token
Content-Type: application/json
{
  "channel": "C0123456789",
  "blocks": [
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "*New order received* 🎉\nOrder #1042 from Acme Corp — $4,200"
      }
    }
  ]
}

Response:

{
  "ok": true,
  "ts": "1715000000.000100",
  "channel": "C0123456789"
}

Save the ts (timestamp) and channel from the response. Together, these uniquely identify the message and are required to update it later.

The blocks array uses Slack's Block Kit — a structured layout system that lets you build rich messages with sections, buttons, images, and dropdowns. Plain text is also accepted but blocks give you far more control.

Updating Messages: chat.update

When a status changes — a build completes, an order ships, an approval is actioned — update the original message rather than posting a new one. This keeps channels clean.

{
  "channel": "C0123456789",
  "ts": "1715000000.000100",
  "blocks": [
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "*Order #1042 — Shipped* ✅\nTracking: UPS 1Z999AA10123456784"
      }
    }
  ]
}

Pass "as_user": true if you want the update to appear as coming from the user rather than the bot.

Listing Channels: conversations.list

Retrieves public and private channels. Useful for letting users select a channel in your app's UI without hardcoding channel IDs.

GET https://slack.com/api/conversations.list?types=public_channel,private_channel&limit=200
Authorization: Bearer xoxb-your-bot-token
{
  "channels": [
    { "id": "C0123456789", "name": "engineering-alerts", "is_private": false },
    { "id": "C0987654321", "name": "finance-approvals", "is_private": true }
  ],
  "response_metadata": {
    "next_cursor": "dGVhbTpDMDYxRkE3OTM="
  }
}

Paginate using the cursor query parameter: pass the next_cursor value from response_metadata as the cursor in your next request. Continue until next_cursor is empty.

Looking Up Users: users.list and users.lookupByEmail

Two options depending on what you have:

users.list — returns all workspace members with pagination. Useful for building a local user cache or populating a dropdown.

GET https://slack.com/api/users.list?limit=200
{
  "members": [
    {
      "id": "U0123456789",
      "is_bot": false,
      "deleted": false,
      "profile": { "email": "sarah@acme.com" }
    }
  ],
  "response_metadata": { "next_cursor": "..." }
}

Filter out bots (is_bot: true) and deactivated users (deleted: true) before storing.

users.lookupByEmail — the faster option when you already know the email. One call, one user.

GET https://slack.com/api/users.lookupByEmail?email=sarah@acme.com
{
  "ok": true,
  "user": { "id": "U0123456789" }
}

Use the returned id directly as the channel in chat.postMessage to send a direct message to that user.

Slash Commands

Slash commands let users trigger actions in your external system by typing /command in any Slack channel. When a user fires one, Slack sends a POST request to your registered endpoint within 3 seconds — if your response takes longer, Slack will show an error.

The Payload

{
  "eventId": "evt_01abc",
  "eventType": "slash_command",
  "eventData": {
    "command": "/report",
    "text": "Q1 2026",
    "keyCommand": "report",
    "argumentCommand": "Q1 2026",
    "userId": "U0123456789",
    "teamId": "T0123456789",
    "channelId": "C0123456789",
    "responseUrl": "https://hooks.slack.com/commands/..."
  }
}

Key fields:

  • command — the slash command itself (e.g., /report)
  • text — everything the user typed after the command
  • keyCommand — the command name without the slash
  • argumentCommand — the arguments portion (everything after the command name)
  • userId — who triggered it
  • responseUrl — a URL you can POST a delayed response to (valid for 30 minutes)

Handling Async Responses

If your command triggers a long-running operation, acknowledge immediately with a simple response, then POST the actual result to responseUrl when ready:

// Immediate acknowledgment (within 3s)
{
  "commandResponse": {
    "text": "Generating your Q1 report, hang tight..."
  }
}
// Delayed response via responseUrl (up to 30 min later)
{
  "commandResponse": {
    "blocks": [
      {
        "type": "section",
        "text": { "type": "mrkdwn", "text": "*Q1 2026 Report*\nRevenue: $2.4M | Growth: +18%" }
      }
    ]
  }
}

Rate Limits

Slack rate-limits the Web API by method, using a tier system:

Tier Limit Typical methods
Tier 1 ~1 req / min users.list, channels.history
Tier 2 ~20 req / min conversations.list
Tier 3 ~50 req / min per channel chat.postMessage
Tier 4 ~100 req / min users.info, users.lookupByEmail

When you exceed a rate limit, Slack returns HTTP 429 with a Retry-After header. Always implement exponential backoff.

When you hit a limit, Slack responds with HTTP 429 and a Retry-After header indicating how many seconds to wait. Always implement retry logic with exponential backoff. For high-volume messaging (bulk notifications, digest sends), queue messages and pace them against the per-channel limit.

Three Common Integration Patterns

Pattern 1: Send a DM to a User by Email

A common need: your backend event has a user's email and you need to reach them directly in Slack.

import requests

SLACK_TOKEN = "xoxb-your-bot-token"
HEADERS = {"Authorization": f"Bearer {SLACK_TOKEN}", "Content-Type": "application/json"}

def send_dm_by_email(email: str, message: str):
    # Step 1: Resolve email → user ID
    lookup = requests.get(
        "https://slack.com/api/users.lookupByEmail",
        params={"email": email},
        headers=HEADERS
    ).json()

    if not lookup.get("ok"):
        raise Exception(f"User not found: {lookup.get('error')}")

    user_id = lookup["user"]["id"]

    # Step 2: Send DM (user ID is used as the channel)
    response = requests.post(
        "https://slack.com/api/chat.postMessage",
        headers=HEADERS,
        json={
            "channel": user_id,
            "blocks": [
                {"type": "section", "text": {"type": "mrkdwn", "text": message}}
            ]
        }
    ).json()

    if not response.get("ok"):
        raise Exception(f"Message failed: {response.get('error')}")

    return response["ts"]  # Save for later updates

Pattern 2: Interactive Approval Message

Post a message with Approve/Decline buttons, then update it once the manager acts.

def post_approval_request(channel: str, request_details: str):
    response = requests.post(
        "https://slack.com/api/chat.postMessage",
        headers=HEADERS,
        json={
            "channel": channel,
            "blocks": [
                {
                    "type": "section",
                    "text": {"type": "mrkdwn", "text": f"*Approval Request*\n{request_details}"}
                },
                {
                    "type": "actions",
                    "elements": [
                        {"type": "button", "text": {"type": "plain_text", "text": "✅ Approve"},
                         "action_id": "approve", "style": "primary"},
                        {"type": "button", "text": {"type": "plain_text", "text": "❌ Decline"},
                         "action_id": "decline", "style": "danger"}
                    ]
                }
            ]
        }
    ).json()
    return {"ts": response["ts"], "channel": response["channel"]}


def resolve_approval(ts: str, channel: str, approved: bool, actioned_by: str):
    status = "✅ Approved" if approved else "❌ Declined"
    requests.post(
        "https://slack.com/api/chat.update",
        headers=HEADERS,
        json={
            "channel": channel,
            "ts": ts,
            "blocks": [
                {
                    "type": "section",
                    "text": {"type": "mrkdwn", "text": f"*Approval Request* — {status}\nActioned by: {actioned_by}"}
                }
            ]
        }
    )

Pattern 3: Slash Command Dispatcher

Route different /commands to the right handler in your backend.

from flask import Flask, request, jsonify

app = Flask(__name__)

HANDLERS = {
    "report":  handle_report_command,
    "ticket":  handle_ticket_command,
    "status":  handle_status_command,
}

@app.route("/slack/commands", methods=["POST"])
def slack_command():
    payload = request.get_json()
    key_command = payload["eventData"]["keyCommand"]
    args = payload["eventData"]["argumentCommand"]
    user_id = payload["eventData"]["userId"]
    response_url = payload["eventData"]["responseUrl"]

    handler = HANDLERS.get(key_command)
    if not handler:
        return jsonify({"commandResponse": {"text": f"Unknown command: `/{key_command}`"}})

    # Acknowledge immediately, process async
    handler(args, user_id, response_url)
    return jsonify({"commandResponse": {"text": "On it — give me a moment..."}})

Building Slack Integrations with Knit

Managing OAuth installs, token storage, token refresh, and multi-workspace support adds significant overhead before you've written a line of business logic. Knit handles the Slack integration infrastructure — auth, token lifecycle, and a normalised API layer — so you can focus on what your integration actually does.

Here's what Knit exposes for Slack:

Send Message

POST to chat.postMessage behind a single Knit endpoint. Pass a channel ID and a blocks array. The response returns ts and channel — both stored by Knit for downstream operations.

Use cases: Order notifications, incident alerts, digest messages, CRM event triggers, approval requests.

Update Message

Updates an existing message using its ts + channel pair. Pass as_user: true to update as the installing user rather than the bot.

Use cases: Live build status boards, approval resolution, updating order/ticket status without channel noise.

List Channels

Wraps conversations.list with cursor-based pagination handled automatically. Returns id, name, and is_private for each channel. Supports filtering by types.

Use cases: Channel pickers in your UI, compliance audits, onboarding automation (add new users to default channels).

List DM IDs

Retrieves the DM channel IDs for users the bot has existing conversations with. Useful for mapping your internal user records to Slack DM channels without repeatedly calling users.lookupByEmail.

Get DM ID from Email

Single call to resolve an email address to a Slack user ID — the equivalent of users.lookupByEmail. Use the returned id as the channel in a Send Message call to DM that user directly.

Use cases: HR onboarding flows, IT support ticket updates, sales/support follow-up DMs.

Register Bot Command (Slash Commands)

Register a slash command and a destination URL. When a user fires the command, Knit forwards the full event payload — including command, text, keyCommand, argumentCommand, userId, channelId, and responseUrl — to your endpoint, signed with an X-Knit-Signature header for verification.

Your endpoint returns a commandResponse object with blocks and/or text, and Knit delivers it back to Slack. For async operations, use the responseUrl from the forwarded payload.

Use cases: /report, /ticket, /status, /approve — any command that needs to query or trigger something in your backend.

What to Build First

If you're starting a Slack integration from scratch, here's a sensible sequence:

  1. Create your Slack app at api.slack.com/apps and set your required scopes.
  2. Implement the OAuth install flow and store bot tokens per workspace.
  3. Start with chat.postMessage — get a working notification flowing before adding complexity.
  4. Add chat.update once you have messages being sent — live-updating messages is one of the highest-value Slack UX patterns.
  5. Add slash commands if your users need to trigger actions from within Slack.
  6. Add Events API subscriptions if you need to react to things happening in Slack.

If you're integrating Slack as one of several tools in a larger product and don't want to manage per-workspace OAuth and token storage for each one, Knit's Slack integration gives you all six of the above capabilities behind a single authenticated API — and adds every other integration you support through the same interface.

API Surface Direction Best for
Web API Your server → Slack Sending messages, reading data, updating content
Events API Slack → Your server Reacting to events in Slack in real time
Incoming Webhooks Your server → Slack Simple one-way alerts to a single fixed channel
Socket Mode Bidirectional (WebSocket) Local development, no public-facing URL available

The most common mistake in Slack integrations is starting with Incoming Webhooks because they're simple, then realising six months later that you need to post to different channels dynamically, update messages, or handle slash commands — and having to rebuild. Start with the Web API unless your use case genuinely only needs fixed-channel notifications.

Frequently Asked Questions

What is the difference between the Slack Web API and the Events API?

The Web API is request-driven: your server calls Slack to send messages, retrieve data, or update content. The Events API is event-driven: Slack calls your server when something happens in a workspace. Most integrations use both — the Web API to act, the Events API to react.

Which Slack API should I use to send a message?

Use chat.postMessage via the Slack Web API. Authenticate with a bot token (xoxb-), POST to https://slack.com/api/chat.postMessage with a channel ID and a blocks or text body. For direct messages, use the recipient's Slack user ID as the channel value.

How do I send a direct message to a Slack user from my application?

First look up the user's Slack ID by calling users.lookupByEmail with their email address. Then call chat.postMessage using that user ID as the channel parameter. The user will receive the message in their DMs from your app's bot.

What are Slack OAuth scopes and which ones do I need?

Scopes are permissions your app requests when a user installs it. For a basic messaging integration you need: chat:write (post messages), users:read.email (look up users by email), channels:read (list channels), and commands (if you're adding slash commands). Only request scopes you actually use.

What is Slack Socket Mode and when should I use it?

Socket Mode lets your app receive Slack events over a WebSocket connection instead of a public HTTP endpoint. Use it during local development when you don't have a public URL, or in production environments behind a firewall. For public-facing production apps, the Events API over HTTP is the standard approach.

Does the Slack Web API have rate limits?

Yes. Slack uses a tier system: chat.postMessage is Tier 3 (~50 requests per minute per channel), conversations.list is Tier 2 (~20 req/min), and users.lookupByEmail is Tier 4 (~100 req/min). Exceeding limits returns HTTP 429 with a Retry-After header. Always implement exponential backoff retry logic.

How do I handle Slack slash commands in my backend?

Register your slash command in your Slack app settings with an endpoint URL. Slack will POST a payload to that URL whenever the command is used. You must respond within 3 seconds — for longer operations, return an immediate acknowledgment and use the responseUrl from the payload to send the actual response asynchronously.

#1 in Ease of Integrations

Trusted by businesses to streamline and simplify integrations seamlessly with GetKnit.