BLOGS

Slack Messaging API Guide (Python, 2026)

March 10, 2026

Learn Slack API send message patterns: post to channels, users, and private channels with Python using chat.postMessage, OAuth, and Slack Conversations API.

Slack Messaging API Guide (Python, 2026)

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

AspectPingramSlack APIs (DIY)
OAuth FlowPre-built auth flowBuild from scratch
Token StorageManaged & encryptedYour database + encryption
Rate LimitsHandled automaticallyTrack and retry in your own app
Time to ShipMinutesDays to weeks
MaintenanceNoneOngoing
CostsFree tierDatabase + compute + engineering time
Best ForShip fast with minimal setupFull 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

  1. Create a Slack App
  2. OAuth Flow
  3. Destination Picker (which channel/user should receive the messages)
  4. Sending Messages
  5. Rate Limits & Error Handling

Step 1: Create a Slack App

  1. Go to api.slack.com/apps and create a new app
  2. Add your Redirect URL (e.g., https://yourapp.com/slack/callback)
  3. Add Bot Token Scopes:
  1. 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:

ErrorCauseFix
channel_not_foundInvalid ID or no accessVerify channel, check bot membership
not_in_channelApp is not a member of target conversationInvite app or join channel (public)
token_revokedUser disconnectedMark invalid, prompt reconnect
invalid_authInvalid token or bad configVerify 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:

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:

Choose DIY if:

Most teams start with Pingram and only build custom when they hit a specific limitation. Try Pingram for free →