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
September 30, 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. 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.

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

Slack chat.scheduleMessage: Limits and Errors We Measured

We called chat.scheduleMessage with a bot token for times from 10 seconds to 121 days ahead, listed and deleted the results, and watched which messages still arrived. The limits, the exact errors, and a delete that returned ok but did not stop the post.

Slack Green Team
Guide

Slack Invite Link Expiration: 30 Days, and How to Check One

Slack invite links expire after 30 days and have no longer option. We made links in a test workspace, deactivated them, and found a signed-out check that tells a live link from a dead one.

Slack Green Team
Guide

Slack Pinned Messages: Where Pins Are Now, and the 100-Pin Limit

In the current Slack desktop and web app, pinned messages sit in a Pins tab at the top of the channel. We pinned, edited, deleted and searched pins in a test workspace, and pinned messages until Slack refused the 101st.

Slack Green Team