Back to Blog
Guide

Slack API: Get Messages From a Channel, With the Output We Got

Call conversations.history, follow next_cursor, then fetch each thread with conversations.replies. We read 40 seeded messages and 6 replies from a test channel and show the script, its output, and what limit, oldest and latest did.

Slack Green Team
September 30, 2026
October 2, 2026
4 min read
Share:
slack api
developers
python

To get messages from a Slack channel with the API, call conversations.history with the channel ID and follow response_metadata.next_cursor until it comes back empty. Thread replies are not in that list. For each message with a reply_count, call conversations.replies with its ts. Use the parent's ts: with a reply's ts, our thread_ts test got back only that one reply. On 30 September 2026 we posted 40 messages and 6 thread replies into a test channel and read them back with the script below. It returned 41 messages in 3 pages, then the 6 replies. The output is copied from that run.

The script we ran

It needs a token with channels:history (groups:history for private channels, im:history and mpim:history for DMs) and the channel ID, which starts with C. Our guide to finding a Slack channel ID shows where to copy it. We used limit=15 so the paging shows up with only 41 messages.

import os, sys, time, requests

TOKEN = os.environ["SLACK_TOKEN"]
CHANNEL = sys.argv[1]
API = "https://slack.com/api/"

def call(method, **params):
    while True:
        r = requests.get(API + method, params=params,
                         headers={"Authorization": f"Bearer {TOKEN}"}, timeout=30)
        if r.status_code == 429:
            time.sleep(int(r.headers.get("Retry-After", 1)))
            continue
        data = r.json()
        if not data["ok"]:
            raise RuntimeError(f"{method}: {data['error']}")
        return data

messages, cursor, page = [], None, 0
while True:
    params = {"channel": CHANNEL, "limit": 15}
    if cursor:
        params["cursor"] = cursor
    data = call("conversations.history", **params)
    page += 1
    messages += data["messages"]
    cursor = data.get("response_metadata", {}).get("next_cursor")
    print(f"page {page}: {len(data['messages'])} messages, has_more={data['has_more']}, "
          f"next_cursor={'yes' if cursor else 'none'}")
    if not cursor:
        break

replies = 0
for m in messages:
    if m.get("reply_count"):
        thread = call("conversations.replies", channel=CHANNEL, ts=m["ts"])
        replies += len(thread["messages"]) - 1
        print(f"thread {m['ts']}: reply_count={m['reply_count']}, "
              f"replies returned={len(thread['messages']) - 1}")

print(f"total: {len(messages)} top-level messages, {replies} thread replies, {page} pages")
print("newest:", messages[0]["text"], "| oldest:", messages[-1]["text"])

Output, run as SLACK_TOKEN=xoxp-... python fetch_history.py C0C5RTM8YKE:

page 1: 15 messages, has_more=True, next_cursor=yes
page 2: 15 messages, has_more=True, next_cursor=yes
page 3: 11 messages, has_more=False, next_cursor=none
thread 1790771773.848419: reply_count=3, replies returned=3
thread 1790771751.551069: reply_count=3, replies returned=3
total: 41 top-level messages, 6 thread replies, 3 pages
newest: seed message 40 of 40 | oldest: <@U0B7L4YK420> has joined the channel

What the output tells you

  • • 41, not 40. The oldest item is the system message <@U0B7L4YK420> has joined the channel, with subtype: channel_join. History returns system messages too. Filter on subtype if you only want what people typed.
  • • Newest first. Page 1 starts at message 40. Reverse the list if you want reading order.
  • • Replies stay in their threads. The 6 replies never showed up in conversations.history. The parent message carries reply_count: 3, latest_reply and reply_users. conversations.replies returns the parent first and then the replies, which is why the script subtracts 1.
  • • has_more and next_cursor agree. Page 3 had has_more=False and an empty cursor. Loop on the cursor, as the script does. What a cursor contains, and which broken cursors fail without an error, is in Slack API pagination.

Each ts is a Unix time in seconds with a 6-digit suffix. It is also the message ID you pass to conversations.replies, chat.update or chat.delete. Paste one into the Slack timestamp tool to see it as a date.

Limit, oldest and latest

We called conversations.history on the same channel with other parameters:

ParametersMessages returnedhas_more
no limit41false
limit=100041false
limit=041false
limit=11true
oldest=ts of message 10, latest=ts of message 209 (messages 11 to 19)false
the same, plus inclusive=true11 (messages 10 to 20)false

oldest and latest leave out the two boundary messages unless you add inclusive=true. Slack's docs put the default at 100, the maximum at 999 and the recommended page size at 200 or fewer. Our limit=1000 call did not return an error. To fetch one day, set oldest and latest to Unix seconds for that day's start and end.

Never appear "away" on Slack again

Cloud-based. No downloads. Works 24/7 even when your laptop is off.

The same thing with slack_sdk

The official Python SDK pages for you when you loop over the response. We ran this with slack_sdk 3.44.1 on the same channel:

import os, sys
from slack_sdk import WebClient

client = WebClient(token=os.environ["SLACK_TOKEN"])
channel = sys.argv[1]

pages = 0
messages = []
for page in client.conversations_history(channel=channel, limit=15):
    pages += 1
    messages += page["messages"]

replies = []
for m in messages:
    if m.get("reply_count"):
        for page in client.conversations_replies(channel=channel, ts=m["ts"]):
            replies += page["messages"][1:]

print(pages, "pages,", len(messages), "messages,", len(replies), "replies")

It printed 3 pages, 41 messages, 6 replies. That matches the plain requests version.

Token type and the 15-message limit

We read the channel with a user token (xoxp-) from an internal app, so it could see every channel the user is in. A bot token (xoxb-) sees only channels the bot has joined. In a channel it has not joined, the call fails with not_in_channel, and the not_in_channel fix covers that.

Slack's changelog of 29 May 2025 cut conversations.history and conversations.replies for commercially distributed apps that are not in the Slack Marketplace. The cut is 1 request a minute and at most 15 messages per call. Internal apps keep the normal Tier 3 limit. We tried to trigger the cut. We turned on public distribution for our test app, revoked the token and installed the app again. Back in the same workspace, three calls with limit=40 in one second each returned 40 messages with has_more=true, and none got a 429. So the cut did not reach an install in the app's own workspace. We did not test an install in a second workspace. For the normal limits, our Slack API rate limit test got 150 conversations.history calls through before the first 429, which came with Retry-After: 10.

FAQ

Can I export a whole channel without code?

Workspace owners and admins can export public channel history from the admin export page. The API route above is for your own scripts, and it reads only what your token can see.

Why do my messages come back with bot_id even though a person sent them?

We posted the seed messages with a user token from our app. conversations.history returned them with user set to the person, plus bot_id, app_id and a bot_profile for the app that posted them. Use user to find the author. Use app_id to tell which messages came through an app.

How far back does history go on the free plan?

Slack's free plan shows the last 90 days of messages, per Slack's plan page. We did not test what the API returns for older, hidden messages.

Always Active

Stop Jiggling Your Mouse.

Join hundreds of remote workers who never worry about their Slack status. Set it up once, stay green forever.

Related Articles

Guide

How to See All Your DMs in Slack, Including the Hidden Ones

The DMs tab lists your direct messages with people. App DMs, Slackbot and DMs with deleted accounts sit elsewhere. We compared the tab with the full list the Slack API returned.

Slack Green Team
Guide

Slack Images Not Showing? 5 Causes We Reproduced

We uploaded images to a Slack test channel and then switched each image setting off, ran /collapse, and posted from a bot that was not in the channel. Five causes, each reproduced.

Slack Green Team
Guide

How to Message Yourself on Slack (and What It Notifies)

Your own name under Direct messages opens a private DM with yourself. We sent it a note, a file, a scheduled message and a reminder, and checked which ones raised a badge.

Slack Green Team