BLOGS
Slack Messaging API Guide (Python, 2026)
Learn Slack API send message patterns: post to channels, users, and private channels with Python using chat.postMessage, OAuth, and Slack Conversations API.
Who is this for?
Python developers wanting to implement Slack notifications.
This guide shows how to do Slack API send message with Python by connecting a Slack workspace then sending notifications to a DM or channel.
You have two options: use Pingram and ship in minutes, or build it yourself with the Slack message API.
Quick Comparison
| Aspect | Pingram | Slack APIs (DIY) |
|---|---|---|
| OAuth Flow | Pre-built auth flow | Build from scratch |
| Token Storage | Managed & encrypted | Your database + encryption |
| Rate Limits | Handled automatically | Track and retry in your own app |
| Time to Ship | Minutes | Days to weeks |
| Maintenance | None | Ongoing |
| Costs | Free tier | Database + compute + engineering time |
| Best For | Ship fast with minimal setup | Full control, custom requirements |
Option 1: Pingram (The Easy Way)
Here’s all you need with Pingram.
1. Pre-Built Connect Slack Button (React)
This React component handles the entire OAuth flow-authorization, token exchange, and secures the token in our servers safely. A user clicks “Connect Slack,” authorizes, and they’re done.
Install the SDK first:
npm install @notificationapi/react
Then render the React component that handles the OAuth flow:
import { NotificationAPIProvider, SlackConnect } from '@notificationapi/react';
<NotificationAPIProvider clientId="YOUR_CLIENT_ID" userId={userId}>
<SlackConnect />
</NotificationAPIProvider>;
That’s it.
2. Send Messages with Python
Install the Python SDK:
pip install pingram-python
Then send a Slack message:
import asyncio
from pingram import Pingram, SenderPostBody, SenderPostBodyTo, SenderPostBodySlack
async def main():
async with Pingram(api_key="pingram_sk_...") as client:
await client.send(sender_post_body=SenderPostBody(
type="new_order",
to=SenderPostBodyTo(id=user_id),
slack=SenderPostBodySlack(
# Plain text:
text="Hello world!",
# Slack block kit:
blocks=[{
"type": "section",
"text": {"type": "mrkdwn", "text": "*Hello world!*"}
}]
)
))
asyncio.run(main())
That’s It
Two code snippets. No OAuth callback endpoints. No encrypted token database. No rate limit queues. Try Pingram →
Option 2: Slack APIs (DIY)
If you need full control or have specific requirements Pingram doesn’t cover yet (like inbound messages or interactive modals), here’s how to build it yourself with Python.
The Steps
- Create a Slack App
- OAuth Flow
- Destination Picker (which channel/user should receive the messages)
- Sending Messages
- Rate Limits & Error Handling
Step 1: Create a Slack App
- Go to api.slack.com/apps and create a new app
- Add your Redirect URL (e.g.,
https://yourapp.com/slack/callback) - Add Bot Token Scopes:
chat:write— required forchat.postMessagechat:write.public— optional, allows posting in public channels without joining firstchannels:readandgroups:read— optional, used if you build a channel pickerim:readandmpim:read— optional, used if you list DM/MPIM destinationschannels:join— optional, only if you want to auto-join public channels viaconversations.joinim:write— optional, used if you open DMs withconversations.openusers:read— optional, only if you need user profiles for display
- Note your Client ID and Client Secret
Step 2: OAuth Flow
Redirect users to Slack’s authorization URL with your scopes and a CSRF-safe state parameter. When they approve, Slack redirects back with a code. Exchange that code for an access token and store it securely.
If token rotation is enabled for your Slack app, you’ll also receive refresh_token and expires_in and must rotate tokens before expiry.
import os
from urllib.parse import urlencode
SLACK_CLIENT_ID = os.environ["SLACK_CLIENT_ID"]
SLACK_CLIENT_SECRET = os.environ["SLACK_CLIENT_SECRET"]
REDIRECT_URI = "https://yourapp.com/slack/callback"
SCOPES = "chat:write,chat:write.public,channels:read,groups:read,im:read,mpim:read,users:read"
def generate_auth_url(user_id: str) -> str:
state = generate_secure_state(user_id)
params = {
"client_id": SLACK_CLIENT_ID,
"scope": SCOPES,
"redirect_uri": REDIRECT_URI,
"state": state
}
return f"https://slack.com/oauth/v2/authorize?{urlencode(params)}"
At this stage, users enter Slack OAuth and install your app into their workspace. After approval, Slack redirects to your callback URL.
Here’s the callback handler using Flask:
import requests
from flask import Flask, request, redirect
app = Flask(__name__)
@app.route("/slack/callback")
def slack_callback():
code = request.args.get("code")
state = request.args.get("state")
user_id = verify_state(state)
if not user_id:
return "Invalid state", 400
response = requests.post(
"https://slack.com/api/oauth.v2.access",
data={
"client_id": SLACK_CLIENT_ID,
"client_secret": SLACK_CLIENT_SECRET,
"code": code,
"redirect_uri": REDIRECT_URI
}
)
data = response.json()
if not data.get("ok"):
return redirect("/settings?slack=error")
store_slack_tokens(user_id, {
"team_id": data["team"]["id"],
"team_name": data["team"]["name"],
"access_token": data["access_token"],
"scope": data["scope"],
# present when token rotation is enabled
"refresh_token": data.get("refresh_token"),
"expires_in": data.get("expires_in"),
# present if app requests incoming webhook support
"incoming_webhook_url": data.get("incoming_webhook", {}).get("url")
})
return redirect("/settings?slack=connected")
For FastAPI, the handler looks similar:
from fastapi import FastAPI, Query
from fastapi.responses import RedirectResponse
import httpx
app = FastAPI()
@app.get("/slack/callback")
async def slack_callback(code: str = Query(...), state: str = Query(...)):
user_id = verify_state(state)
if not user_id:
return RedirectResponse("/settings?slack=error", status_code=302)
async with httpx.AsyncClient() as client:
response = await client.post(
"https://slack.com/api/oauth.v2.access",
data={
"client_id": SLACK_CLIENT_ID,
"client_secret": SLACK_CLIENT_SECRET,
"code": code,
"redirect_uri": REDIRECT_URI
}
)
data = response.json()
if not data.get("ok"):
return RedirectResponse("/settings?slack=error", status_code=302)
await store_slack_tokens(user_id, {
"team_id": data["team"]["id"],
"team_name": data["team"]["name"],
"access_token": data["access_token"],
"scope": data["scope"],
"refresh_token": data.get("refresh_token"),
"expires_in": data.get("expires_in"),
"incoming_webhook_url": data.get("incoming_webhook", {}).get("url")
})
return RedirectResponse("/settings?slack=connected", status_code=302)
Step 3: Destination Picker
After OAuth, fetch available destinations so users can choose where notifications go. You can use the official slack_sdk library:
from slack_sdk import WebClient
def get_slack_destinations(user_id: str) -> dict:
connection = get_slack_connection(user_id)
client = WebClient(token=connection["access_token"])
response = client.conversations_list(
exclude_archived=True,
types="public_channel,private_channel",
limit=200
)
channels = [
{
"id": conv["id"],
"name": conv["name"],
"is_private": conv["is_private"]
}
for conv in response.get("channels", [])
]
return {"channels": channels}
Store the user’s selection:
update_user(user_id, {"slack_channel": selected_destination_id})
If you support Slack API send message to user, store a user destination separately and open a DM with conversations.open before posting.
Step 4: Sending Messages
Use the user’s stored destination preference. This is a practical Slack API post message example with chat.postMessage:
from slack_sdk import WebClient
def notify_user(user_id: str, notification: dict) -> dict:
user = get_user(user_id)
connection = get_slack_connection(user_id)
client = WebClient(token=connection["access_token"])
result = client.chat_postMessage(
channel=user["slack_channel"],
text=notification["fallback_text"],
blocks=[
# block kit
]
)
return {"sent": True, "ts": result["ts"]}
For Slack schedule message flows, use chat.scheduleMessage with a Unix timestamp:
import time
client.chat_scheduleMessage(
channel=user["slack_channel"],
text=notification["fallback_text"],
post_at=int(time.time()) + 3600 # 1 hour from now
)
If you prefer using requests directly instead of the SDK:
import requests
def send_slack_message(access_token: str, channel: str, text: str, blocks: list = None) -> dict:
response = requests.post(
"https://slack.com/api/chat.postMessage",
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
},
json={
"channel": channel,
"text": text,
"blocks": blocks
}
)
data = response.json()
if not data.get("ok"):
raise Exception(f"Slack API error: {data.get('error')}")
return {"sent": True, "ts": data["ts"]}
Step 5: Rate Limits & Error Handling
chat.postMessage uses Slack’s special rate limit tier. A safe baseline is about 1 message/second/channel, plus a workspace-wide cap. Respect Retry-After on HTTP 429 responses and queue retries when needed.
import time
from slack_sdk.errors import SlackApiError
def send_with_retry(client: WebClient, channel: str, text: str, user_id: str, max_retries: int = 3):
for attempt in range(max_retries):
try:
return client.chat_postMessage(channel=channel, text=text)
except SlackApiError as e:
if e.response.status_code == 429:
retry_after = int(e.response.headers.get("Retry-After", 1))
time.sleep(retry_after)
elif e.response["error"] in ("token_revoked", "account_inactive"):
mark_connection_disconnected(user_id)
raise
else:
raise
raise Exception("Max retries exceeded")
Users can revoke your app’s access anytime from Slack settings. When you hit token_revoked or account_inactive, mark the connection as disconnected and prompt reconnect. Treat invalid_auth as a bad token/config error and avoid blind retries.
Pitfalls
Public channels: With chat:write.public, apps can usually post to public channels without joining. If your flow does require joining, call conversations.join and request the channels:join scope.
Private channels: For Slack API send message to private channel, your app must be invited first (/invite @yourbot). Bots cannot self-join private channels.
Sending to users: Don’t send by username (deprecated). For Slack API send message to specific user, either post to their App Home/Slackbot thread by user ID or open a DM with conversations.open and send to the returned D... channel ID.
def send_dm_to_user(client: WebClient, user_id: str, text: str):
# Open a DM channel with the user
response = client.conversations_open(users=[user_id])
dm_channel = response["channel"]["id"]
# Send the message
return client.chat_postMessage(channel=dm_channel, text=text)
Destination picker pagination: conversations.list is paginated. If a workspace has many channels, follow response_metadata.next_cursor until empty, or users won’t see all channels.
def get_all_channels(client: WebClient) -> list:
channels = []
cursor = None
while True:
response = client.conversations_list(
exclude_archived=True,
types="public_channel,private_channel",
limit=200,
cursor=cursor
)
channels.extend(response.get("channels", []))
cursor = response.get("response_metadata", {}).get("next_cursor")
if not cursor:
break
return channels
Token revocation: Users can disconnect your app anytime from Slack settings. Always handle token_revoked and invalid_auth errors gracefully.
Common errors:
| Error | Cause | Fix |
|---|---|---|
channel_not_found | Invalid ID or no access | Verify channel, check bot membership |
not_in_channel | App is not a member of target conversation | Invite app or join channel (public) |
token_revoked | User disconnected | Mark invalid, prompt reconnect |
invalid_auth | Invalid token or bad config | Verify token/config, reconnect if needed |
Slack Webhook URL vs Slack API Post Message
If you’re comparing a Slack webhook URL to the Web API:
- Incoming Webhooks are great for simple one-channel posting and quick setup.
chat.postMessage+ OAuth is better when each user chooses their own workspace/channel and you need richer controls (threading, scheduling, auth, destination settings).
For most apps that need flexible Slack messaging, the OAuth + Slack Conversations API route is the right long-term architecture.
Which Should You Choose?
Choose Pingram if:
- You want to ship fast
- You don’t need inbound messages or interactive modals (coming soon)
- You’d rather not maintain OAuth infrastructure
Choose DIY if:
- You need features Pingram doesn’t support yet
- You want complete control over the implementation
Most teams start with Pingram and only build custom when they hit a specific limitation. Try Pingram for free →