Automate Slack channel reports by calling conversations.history to fetch messages and conversations.replies for thread data, then aggregating stats and posting a summary via chat.postMessage. Critical 2025-2026 change: non-Marketplace apps are now limited to 1 request/minute and 15 messages/request on conversations.history (effective March 3, 2026). This makes naive polling for active channels impossibly slow — Marketplace approval or internal app status is the only sustainable path for production channel analytics.
API Quick Reference
Bot Token (xoxb-)
1 req/min (Tier 1, non-Marketplace) or 50+/min (Tier 3, internal/Marketplace)
JSON
Available
Understanding the Slack API
Slack's conversations.history method returns messages from a channel in reverse chronological order (newest first). Combined with conversations.replies for thread data and users.info for member details, it enables building rich channel activity reports. The architecture is REST-based: POST to https://slack.com/api/conversations.history with a bot token and channel ID.
The critical 2025-2026 change is Slack's rate limit restructuring for data-exfiltration protection. As of May 29, 2025 (new apps) and March 3, 2026 (existing non-Marketplace apps), conversations.history and conversations.replies moved from Tier 3 (50+/min, 1,000 objects/request) to Tier 1 (1/min, 15 objects/request) for non-Marketplace apps. For an active channel with 200 messages/day, fetching them all now takes 14+ API calls and 14+ minutes — effectively unusable for real-time reports.
Internal customer-built apps (installed only in your own workspace) and Marketplace-listed apps are unaffected and retain full Tier 3 access. If you are building a channel report for your own company's Slack, create an internal app — not a distributed app. Official docs: https://docs.slack.dev/apis/web-api/rate-limits
https://slack.com/apiSetting Up Slack API Authentication
For channel reports, the bot needs channels:history to read messages, channels:read to list channels, and chat:write to post the report. Build as an internal app (installed only in your workspace) to retain Tier 3 rate limits on conversations.history — non-Marketplace distributed apps are now limited to 1 request/minute, making large channel analysis impractical.
- 1Go to https://api.slack.com/apps and click Create New App > From scratch.
- 2Select Internal to make this an internal app (critical for retaining Tier 3 rate limits).
- 3Click OAuth & Permissions in the left sidebar. Add Bot Token Scopes: channels:history, channels:read, chat:write, users:read.
- 4If you need private channel data, also add groups:history and groups:read.
- 5Click Install to Workspace and authorize.
- 6Copy the Bot User OAuth Token (xoxb-).
- 7Invite the bot to each channel you want to report on: /invite @YourBotName in each channel.
- 8Store SLACK_BOT_TOKEN in environment variables.
1import os2import requests34BOT_TOKEN = os.environ["SLACK_BOT_TOKEN"]5HEADERS = {"Authorization": f"Bearer {BOT_TOKEN}"}67# Verify token and check rate limit tier8resp = requests.post("https://slack.com/api/auth.test", headers=HEADERS)9data = resp.json()10if data.get("ok"):11 print(f"Authenticated as {data['user']} in workspace {data['team']}")12else:13 raise ValueError(f"Auth failed: {data.get('error')}")Security notes
- •Store SLACK_BOT_TOKEN in environment variables — never commit to source control.
- •Request only channels:history and channels:read — avoid admin-level scopes.
- •For production channel analytics, use an internal app to retain Tier 3 rate limits.
- •Audit which channels your bot has been invited to — it can read all messages in those channels.
- •Do not log full message content in your report pipeline — aggregate only, to protect member privacy.
Key endpoints
/conversations.historyReturns messages from a channel. Newest messages first. Paginate with cursor from response_metadata.next_cursor. Rate limit: Tier 1 (1 req/min, max 15 messages) for non-Marketplace apps; Tier 3 (50+/min, max 1,000 messages) for internal/Marketplace apps.
| Parameter | Type | Required | Description |
|---|---|---|---|
channel | string | required | Channel ID (C...) |
limit | number | optional | Max messages per page. Tier 1 apps: max 15. Tier 3 apps: max 1,000. |
oldest | string | optional | Unix timestamp — only return messages after this time |
latest | string | optional | Unix timestamp — only return messages before this time |
Request
1{"channel":"C01234567","limit":200,"oldest":"1746518400","latest":"1746604800"}Response
1{"ok":true,"messages":[{"type":"message","user":"U01234","text":"Hello world","ts":"1746600000.000100","reply_count":3,"reactions":[{"name":"thumbsup","count":2}]}],"has_more":true,"response_metadata":{"next_cursor":"bmV4dF90czoxNzQ2NTk"}}/conversations.repliesReturns all replies in a thread. Provide the thread_ts of the parent message. Also subject to Tier 1 limits for non-Marketplace apps since March 2026.
| Parameter | Type | Required | Description |
|---|---|---|---|
channel | string | required | Channel ID |
ts | string | required | Timestamp of the parent message (thread_ts) |
Request
1{"channel":"C01234567","ts":"1746600000.000100"}Response
1{"ok":true,"messages":[{"type":"message","user":"U01234","text":"Original message","ts":"1746600000.000100"},{"type":"message","user":"U05678","text":"Reply in thread","ts":"1746600060.000200","parent_user_id":"U01234"}],"has_more":false}/chat.postMessagePosts the aggregated report summary to a reporting channel. Use Block Kit for formatted tables and stat sections.
| Parameter | Type | Required | Description |
|---|---|---|---|
channel | string | required | Channel to post the report to |
blocks | array | optional | Block Kit blocks for formatted report display |
Request
1{"channel":"C09876543","text":"Weekly Channel Report","blocks":[{"type":"section","text":{"type":"mrkdwn","text":"*#team-general — Weekly Report*\n*Messages:* 342 | *Active users:* 28 | *Top threads:* 12"}}]}Response
1{"ok":true,"ts":"1746604800.000100","channel":"C09876543"}Step-by-step automation
Fetch Channel Messages with Pagination
Why: conversations.history returns a maximum of 1,000 messages per call (or 15 for Tier 1 apps) — you must paginate using cursors to get all messages in a time window.
Call conversations.history with your channel ID, oldest and latest Unix timestamps defining the report window, and a limit. If has_more is true in the response, make another call using the next_cursor from response_metadata. For Tier 1 apps (non-Marketplace), you can only make 1 request per minute — set oldest/latest to a narrow window and accept that deep history is unavailable without Tier 3 access.
1# Get last 24 hours of messages (replace timestamps)2START=$(date -d '24 hours ago' +%s)3END=$(date +%s)4curl -X POST \5 -H "Authorization: Bearer $SLACK_BOT_TOKEN" \6 -H "Content-Type: application/json" \7 -d "{\"channel\":\"C01234567\",\"limit\":200,\"oldest\":\"$START\",\"latest\":\"$END\"}" \8 "https://slack.com/api/conversations.history"Pro tip: If you're on Tier 1 (non-Marketplace), your report window is effectively limited to 15 messages (or one page at 1 req/min). Switch to an internal app or submit for Marketplace listing to access Tier 3 (1,000 messages/request, 50+ requests/minute).
Expected result: An array of message objects. Each message has user, text, ts, reply_count, and reactions fields. Newest messages are returned first.
Aggregate Channel Statistics
Why: Raw message objects need to be aggregated into meaningful stats before posting the report.
From the messages array, calculate: total message count, unique active user count, message count per user (top contributors), number of threads (messages with reply_count > 0), most-reacted messages, and messages per day for trending. Process all this in memory — no additional API calls needed for basic stats.
1# No direct cURL equivalent — this is application logic2# Pipe the conversations.history response through jq for quick stats:3curl -s -X POST \4 -H "Authorization: Bearer $SLACK_BOT_TOKEN" \5 -H "Content-Type: application/json" \6 -d '{"channel":"C01234567","limit":200}' \7 "https://slack.com/api/conversations.history" \8 | python3 -c "import sys,json; d=json.load(sys.stdin); msgs=d.get('messages',[]); print(f'Total: {len(msgs)}, Users: {len(set(m.get(\"user\",\"\") for m in msgs))}')"Pro tip: Store aggregated stats in a database after each report run. This lets you show week-over-week comparisons without re-fetching message history each time.
Expected result: A stats object with message totals, unique users, thread counts, reactions, and top contributors — ready to format into a report.
Format and Post the Report with Block Kit
Why: A formatted Block Kit message is far more readable than a plain text summary and works within Slack's content limits.
Build a Block Kit message with a header section, stats fields, top contributors list, and a context footer. Keep total block count under 50 and text under 3,000 characters per section. Post to a dedicated #reports channel using chat.postMessage. Save the ts to enable threaded updates (e.g., adding drill-down data).
1curl -X POST \2 -H "Authorization: Bearer $SLACK_BOT_TOKEN" \3 -H "Content-Type: application/json" \4 -d '{5 "channel": "C09876543",6 "text": "Weekly Channel Report: #team-general",7 "blocks": [8 {"type":"header","text":{"type":"plain_text","text":"Weekly Report: #team-general"}},9 {"type":"section","fields":[10 {"type":"mrkdwn","text":"*Messages:*\n342"},11 {"type":"mrkdwn","text":"*Active Users:*\n28"},12 {"type":"mrkdwn","text":"*Threads:*\n47"},13 {"type":"mrkdwn","text":"*Reactions:*\n183"}14 ]},15 {"type":"context","elements":[{"type":"mrkdwn","text":"Report period: May 1-7, 2026"}]}16 ]17 }' \18 "https://slack.com/api/chat.postMessage"Pro tip: Save the report message ts and post drill-down details (e.g., most-reacted messages, day-by-day breakdown) as thread replies to avoid a single overwhelming report message.
Expected result: A formatted report appears in the reporting channel with message counts, user activity, thread stats, and top contributors. The response includes ts for threading follow-ups.
Schedule the Report with a Cron Job
Why: Reports are most useful when they run automatically — weekly Monday morning or daily EOD, not on demand.
Schedule your report script using cron (Linux), Task Scheduler (Windows), or a scheduling service (GitHub Actions, Render cron, Railway cron). For weekly reports, aim for Monday morning so teams start the week with last week's data. For daily digests, run EOD (5pm local time). Use the oldest and latest timestamp parameters to define the exact reporting window.
1# Add to crontab for weekly Monday 9am report:2# crontab -e3# 0 9 * * 1 /usr/bin/python3 /path/to/automate_slack_reports.py45# Or use Slack's chat.scheduleMessage to pre-schedule the report posting:6curl -X POST \7 -H "Authorization: Bearer $SLACK_BOT_TOKEN" \8 -H "Content-Type: application/json" \9 -d '{"channel":"C09876543","text":"Generating report...","post_at":1747044000}' \10 "https://slack.com/api/chat.scheduleMessage"Pro tip: Use chat.scheduleMessage if you want Slack to deliver the message at a specific time even if your server is temporarily unavailable. However, note that scheduled messages cannot be edited after creation — only deleted and recreated.
Expected result: Reports run automatically on schedule. Each channel gets a formatted report posted to the designated reporting channel.
Complete working code
A complete weekly channel report automation that fetches message history with pagination, aggregates stats (messages, users, threads, reactions, top contributors), builds a Block Kit report, and posts it to a reporting channel. Includes rate-limit handling for both Tier 1 and Tier 3 apps.
1import os, time, logging, requests2from collections import defaultdict3from datetime import datetime, timedelta, timezone45logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")67BOT_TOKEN = os.environ["SLACK_BOT_TOKEN"]8HEADERS = {"Authorization": f"Bearer {BOT_TOKEN}"}9REPORT_CHANNEL = os.environ["SLACK_REPORT_CHANNEL_ID"]10CHANNELS = [("C01234567", "team-general"), ("C02345678", "engineering")]1112def fetch_messages(channel_id, days_back=7):13 oldest = (datetime.now(timezone.utc) - timedelta(days=days_back)).timestamp()14 messages, cursor = [], None15 while True:16 payload = {"channel": channel_id, "limit": 200, "oldest": str(oldest)}17 if cursor:18 payload["cursor"] = cursor19 resp = requests.post("https://slack.com/api/conversations.history",20 headers=HEADERS, json=payload)21 if resp.status_code == 429:22 wait = int(resp.headers.get("Retry-After", 60)) + 123 logging.warning(f"Rate limited — sleeping {wait}s (Tier 1 limit)")24 time.sleep(wait)25 continue26 data = resp.json()27 if not data.get("ok"):28 logging.error(f"conversations.history error: {data.get('error')}")29 break30 messages.extend(data.get("messages", []))31 if not data.get("has_more"):32 break33 cursor = data["response_metadata"]["next_cursor"]34 time.sleep(1.2)35 return messages3637def build_stats(messages, channel_name):38 user_counts, messages_per_day = defaultdict(int), defaultdict(int)39 threads = reactions = 040 for m in messages:41 user_counts[m.get("user", "bot")] += 142 if m.get("reply_count", 0) > 0:43 threads += 144 for r in m.get("reactions", []):45 reactions += r.get("count", 0)46 day = datetime.fromtimestamp(float(m.get("ts", 0)), tz=timezone.utc).strftime("%Y-%m-%d")47 messages_per_day[day] += 148 top = sorted(user_counts.items(), key=lambda x: x[1], reverse=True)[:3]49 return {"name": channel_name, "messages": len(messages),50 "users": len(user_counts), "threads": threads,51 "reactions": reactions, "top": top,52 "avg": round(len(messages) / max(len(messages_per_day), 1), 1)}5354def post_report(stats, period):55 top_text = "\n".join(f"<@{u}>: {c}" for u, c in stats["top"]) or "None"56 blocks = [57 {"type": "header", "text": {"type": "plain_text", "text": f"Weekly: #{stats['name']}"}},58 {"type": "section", "fields": [59 {"type": "mrkdwn", "text": f"*Messages:*\n{stats['messages']}"},60 {"type": "mrkdwn", "text": f"*Users:*\n{stats['users']}"},61 {"type": "mrkdwn", "text": f"*Threads:*\n{stats['threads']}"},62 {"type": "mrkdwn", "text": f"*Reactions:*\n{stats['reactions']}"}63 ]},64 {"type": "section", "text": {"type": "mrkdwn", "text": f"*Top:*\n{top_text}"}},65 {"type": "context", "elements": [{"type": "mrkdwn",66 "text": f"Period: {period} | Avg {stats['avg']}/day"}]}67 ]68 resp = requests.post("https://slack.com/api/chat.postMessage",69 headers=HEADERS,70 json={"channel": REPORT_CHANNEL,71 "text": f"Report: #{stats['name']}", "blocks": blocks})72 return resp.json()7374if __name__ == "__main__":75 period = f"Last 7 days ending {datetime.now(timezone.utc).strftime('%Y-%m-%d')}"76 for cid, cname in CHANNELS:77 msgs = fetch_messages(cid, 7)78 stats = build_stats(msgs, cname)79 result = post_report(stats, period)80 logging.info(f"Report for #{cname}: {result.get('ok')} ts={result.get('ts')}")81 time.sleep(1.5)82Error handling
HTTP 429, body: {"ok":false,"error":"ratelimited"}, header Retry-After: 60conversations.history rate limit exceeded. For Tier 1 apps (non-Marketplace), limit is 1 request/minute. The Retry-After header is typically 60 seconds.
Read Retry-After and sleep for that duration plus 1 second. For large channel reports, consider switching to an internal app (Tier 3: 50+/min) or batching your reports during off-peak hours.
Sleep for Retry-After + 1 second, then retry the same paginated request.
{"ok":false,"error":"not_in_channel"}The bot is not a member of the channel it's trying to read history from.
Invite the bot to the channel: /invite @YourBotName in Slack. The bot must be in the channel to read its history.
Do not retry — invite the bot to the channel first.
{"ok":false,"error":"missing_scope","needed":"channels:history"}The bot token lacks the channels:history scope (or groups:history for private channels).
Add channels:history (and groups:history for private channels) to OAuth & Permissions > Bot Token Scopes and reinstall the app.
Do not retry — fix scopes and reinstall.
{"ok":false,"error":"msg_blocks_too_long"}The report Block Kit payload exceeds the ~13,000-character practical limit (even with fewer than 50 blocks).
Truncate long top-contributor lists, reduce field count in the report, or split into multiple messages posted as thread replies.
Do not retry — reduce payload size.
Rate Limits for Slack API
| Scope | Limit | Window |
|---|---|---|
| conversations.history (non-Marketplace apps, since March 3, 2026) | 1 request/minute, 15 objects max | per minute per workspace |
| conversations.history (internal/Marketplace apps) | 50+ requests/minute, 1,000 objects max | per minute per workspace |
| chat.postMessage | 1 message | per second per channel |
1import time23def conversations_history_with_retry(token, channel, oldest, cursor=None):4 import requests5 headers = {'Authorization': f'Bearer {token}'}6 payload = {'channel': channel, 'limit': 200, 'oldest': str(oldest)}7 if cursor:8 payload['cursor'] = cursor9 for _ in range(5):10 resp = requests.post('https://slack.com/api/conversations.history',11 headers=headers, json=payload)12 if resp.status_code == 429:13 wait = int(resp.headers.get('Retry-After', 60)) + 114 time.sleep(wait)15 continue16 return resp.json()17 raise RuntimeError('Max retries exceeded')- Build channel reports as internal apps (single-workspace install) to retain Tier 3 (50+/min) rate limits on conversations.history.
- Add 1.2-second delays between pagination calls even on Tier 3 — bursting through all pages consecutively can hit per-minute limits.
- Cache aggregated stats in a database after each report run — avoids re-fetching full message history on reruns.
- Schedule reports during off-peak hours (early morning, late evening) when workspace activity is low to minimize API quota competition.
- For large active channels, consider tracking new messages incrementally (using a stored latest_ts) rather than fetching all messages each report cycle.
Security checklist
- Store SLACK_BOT_TOKEN in environment variables — never commit to source control.
- Build as an internal app to avoid the Marketplace approval process and retain Tier 3 rate limits for your own workspace.
- Request only channels:history and channels:read — do not add admin or user-level scopes.
- Do not log full message content from channels in your report pipeline — aggregate only (counts, user IDs) to protect privacy.
- Limit the bot's membership to only the channels you intend to report on — an over-invited bot can read all messages in every channel it's in.
- Use audit logging in your app to track which channels were queried and when.
Automation use cases
Daily Standup Digest
intermediateAggregate all standup messages from a channel posted between 9am-10am daily and compile them into a structured daily digest.
Cross-Channel Trend Analysis
advancedCompare activity across 10+ channels and post a ranking of most active vs. quietest channels with week-over-week trends.
Sentiment Tracking
advancedRun message text through a sentiment API (OpenAI, AWS Comprehend) and include a positivity score in weekly channel reports.
Response Time Reporting
intermediateCalculate average time between a message and its first reply in support channels to measure team responsiveness.
No-code alternatives
Don't want to write code? These platforms can automate the same workflows visually.
Zapier
Free tier availableZapier doesn't natively aggregate channel history, but it can trigger scheduled workflows that call Slack API endpoints via HTTP Request actions.
- + Scheduled triggers
- + HTTP Request action for API calls
- - Complex to implement pagination and aggregation
- - Expensive for API-heavy workflows
- - Not purpose-built for analytics
Make (formerly Integromat)
Free tier available (1,000 operations/month)Make has a Slack module and can chain conversations.history calls with data aggregation steps, but implementing proper pagination and rate-limit handling requires careful scenario design.
- + Visual workflow builder
- + Supports iterators for pagination
- + More affordable than Zapier
- - Rate limit handling requires custom logic
- - Complex for multi-channel reports
- - Execution limits
n8n
Free self-hosted; Cloud from €20/monthn8n's Slack node and code node combination can implement full channel report workflows including pagination, aggregation, and formatted posting.
- + Full code node for complex logic
- + Self-hostable
- + Built-in Slack node
- - Complex setup for production analytics
- - Requires coding for aggregation logic
- - Self-hosting for production
Best practices
- Build as an internal app (not for distribution) to retain Tier 3 rate limits — the March 2026 non-Marketplace limit of 1 req/min makes real-time channel analytics impractical for distributed apps.
- Use oldest and latest Unix timestamps to scope conversations.history requests to your report window — don't fetch all messages then filter client-side.
- Store aggregated stats in a database after each report run — enables week-over-week comparisons without re-fetching history.
- Paginate through all results using the cursor from response_metadata.next_cursor until has_more is false.
- Post reports as threaded replies if running multiple channel reports — keeps the reporting channel readable.
- Always check ok: false in responses — Slack returns application errors as HTTP 200, not HTTP 4xx.
- Schedule reports during off-peak hours to minimize rate limit conflicts with interactive bot usage.
Ask AI to help
Copy one of these prompts to get a personalized, working implementation.
I'm building a Slack channel report automation using Python. My bot token has channels:history and chat:write scopes. I need to: 1) call conversations.history with oldest/latest Unix timestamps to fetch a 7-day message window, 2) paginate using response_metadata.next_cursor until has_more is false, 3) aggregate stats (message count, unique users, threads, reactions, top 3 contributors), 4) build a Block Kit report message with sections and fields, and 5) post it to a reporting channel. Important: I'm on Tier 1 (non-Marketplace) limits — 1 req/min on conversations.history — so I need to handle 429 with Retry-After. Use Python with requests library.
Build a Slack channel analytics dashboard. Features: channel selector showing all channels the bot has been invited to; date range picker for the report window; stats cards showing total messages, unique users, threads, reactions, and average messages/day; top contributors bar chart; day-by-day message volume line chart; send-to-Slack button that posts the current view as a Block Kit report; and a scheduler for automated weekly reports with configuration for target report channel and report time.
Frequently asked questions
Is the Slack API free for channel reports?
Yes — the API itself is free. However, the conversations.history rate limit for non-Marketplace apps dropped to 1 request/minute in March 2026, making it extremely slow for large channels. Internal apps (single-workspace install) retain full Tier 3 access (50+/min, 1,000 messages/request).
What happened to conversations.history rate limits in 2025-2026?
Slack moved conversations.history and conversations.replies from Tier 3 (50+/min, 1,000 objects) to Tier 1 (1/min, 15 objects) for non-Marketplace commercially-distributed apps. New apps were affected from May 29, 2025; existing non-Marketplace installs from March 3, 2026. Internal customer-built apps and Marketplace apps are unaffected.
How do I get Tier 3 access for conversations.history?
Two paths: 1) Build an internal app — install it only in your own workspace, not distributed. Internal apps retain full Tier 3 access. 2) Submit your app to the Slack Marketplace — approved apps also retain Tier 3. For most teams building internal tooling, the internal app route is faster and easier.
What happens when I hit the rate limit?
Slack returns HTTP 429 with body {ok: false, error: 'ratelimited'} and a Retry-After header (typically 60 seconds for Tier 1 limits). Sleep for Retry-After + 1 second before retrying. The limit is per-method, per-workspace, per-minute — a 429 on conversations.history doesn't affect chat.postMessage.
Can I fetch message history from private channels?
Yes, but you need the groups:history scope (not channels:history). The bot must also be invited to the private channel. Private channels start with C... but can be distinguished by their is_private field in the conversations.list response.
How far back can conversations.history go?
There is no hardcoded historical limit in the API — it depends on your Slack plan's message retention policy. Free Slack workspaces retain only 90 days. Paid plans retain messages based on your org's retention settings (often unlimited). Use the oldest parameter to scope your request to the desired window.
Can RapidDev help build a custom Slack analytics system?
Yes. RapidDev has built 600+ apps including Slack analytics dashboards with message volume tracking, sentiment analysis, and automated weekly digest reports. Visit rapidevelopers.com for a free consultation.
Need this automated?
Our team has built 600+ apps with API automations. We can build this for you.
Book a free consultation