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.
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:
The OAuth Install Flow
- Direct the user to Slack's authorization URL with your
client_id, requestedscopes, and aredirect_uri. - User approves the app and is redirected back to your
redirect_uriwith a temporarycode. - Your server exchanges the
codefor an access token viahttps://slack.com/api/oauth.v2.access. - Store the
access_token(andteam_id) securely. This token doesn't expire — but users can revoke it, and you should handletoken_revokedevents.
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/jsonEvery 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 commandkeyCommand— the command name without the slashargumentCommand— the arguments portion (everything after the command name)userId— who triggered itresponseUrl— 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:
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 updatesPattern 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:
- Create your Slack app at api.slack.com/apps and set your required scopes.
- Implement the OAuth install flow and store bot tokens per workspace.
- Start with
chat.postMessage— get a working notification flowing before adding complexity. - Add
chat.updateonce you have messages being sent — live-updating messages is one of the highest-value Slack UX patterns. - Add slash commands if your users need to trigger actions from within Slack.
- 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.
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.





.webp)
