Choosing Slack Bot OAuth Scopes for Reading Channel History
A decision guide for Slack bot OAuth scopes to read channel history. Compare bot_user with broad vs limited scopes, trade-offs for admin approval, and how to validate conversations.history with a test token and channel ID.
12 Oct 2025, 21:15 UTC

The problem: reading history without forcing a user context
You need a Slack bot to read past messages in channels for indexing, compliance export, or alerting. The decision is which OAuth scopes to request at install time. Request too much and workspace admins block the install. Request too little and conversations.history returns empty or errors.
The useful takeaway is to anchor the decision on bot_user versus user token access and on how narrowly you can scope conversations:history. A bot token with bot_user and conversations:history is the supported pattern for server-side reads without requiring a logged-in user.
Decision constraints
Slack separates bot tokens and user tokens. A bot token is issued to the bot_user, an automated identity that acts independently of any human. A user token inherits the permissions of the installing user.
conversations.history requires the bot to be a member of the channel. Membership is granted by inviting the bot with /invite @BotName or via conversations.join. The API will not return messages from channels the bot cannot see, even with the correct scope.
Scope changes are not retroactive. Adding or removing scopes requires re-installation and admin consent. Removing bot_user forces a full reinstall.
Supported scope options
| Option | Scopes | What it allows | Approval friction |
|---|---|---|---|
| Bot read broad | bot_user, channels:read, conversations:history, conversations:read | List channels and read history in public channels the bot joins. channels:read surfaces channel metadata to admins in the install prompt. | High. Admins see broad read permissions and may require justification. |
| Bot read limited | bot_user, conversations:history, conversations:metadata | Read history only in channels the bot is already a member of. No channel listing. conversations:metadata allows inspecting channel properties without listing all. | Lower. Prompt is focused on history access for specific channels. |
| User token minimal | users:read, conversations:history as user | Reads history as the installing user. Requires user to stay present and token refresh handling. | Medium to high. User token prompts are more sensitive and expire. |
Trade-offs
Broader scopes increase coverage but increase approval friction. channels:read makes the install look like a workspace-wide data collector. Many workspaces reject that for a bot that only needs a few channels.
Narrower scopes reduce friction but require operational discipline. With only conversations:history and conversations:metadata, you cannot discover channel IDs programmatically. You must store channel IDs from configuration or an initial setup step.
bot_user is required for a stable server-side bot. User tokens tie access to a person and break when that person leaves or changes permissions. The cost is that bot_user scope removal requires re-installation and admin consent.
Rate limits apply per bot. conversations.history is limited to 1 request per second per bot. Exceeding returns 429. Design backfills with delays or pagination using next_cursor.
Concrete validation
Validate scope coverage with a test install and a direct API call. This confirms the bot can actually read the messages you expect.
Create a new Slack app in api.slack.com/apps, enable bot_user, and add conversations:history and conversations:metadata under Bot Token Scopes. Install to a test workspace and note the Bot User OAuth Token, xoxb-...
Invite the bot to a test channel. Run a request from a local terminal where you have curl and the token available as an environment variable. The token must not be logged.
curl -X GET "https://slack.com/api/conversations.history?channel=CHANNEL_ID&limit=100" \
-H "Authorization: Bearer $BOT_TOKEN" \
-H "Content-Type: application/x-www-form-urlencoded"Replace CHANNEL_ID with the 9-character channel ID, e.g. C012AB3CD. Replace $BOT_TOKEN with your bot token. Required permission is a valid bot token with conversations:history granted and the bot is a member of CHANNEL_ID.
Expected checks: HTTP 200 with JSON containing ok true. Inspect the messages array length and timestamps. If ok is false and error is not_in_channel, the bot is not a member. If error is missing_scope, the install lacks conversations:history.
Compare returned timestamps with the channel's visible history in Slack web to confirm coverage. If you expect messages older than the default window, paginate with next_cursor.
Risks: leaking the bot token exposes workspace data. Rate limit 429 will occur if you poll faster than 1 request per second. Removing bot_user later requires re-installation and admin consent, which causes downtime.
Limitations to plan for
conversations.history only returns messages the bot can see. Private channels require an explicit invite. Threads are returned as separate items with thread_ts.
Message content is subject to Slack's data retention settings. Deleted messages are not returned.
Scope changes are not hot-swapped. Any change to bot_user or conversations:history requires reinstall.
Use the narrowest scope that satisfies the channel list you need to read, store channel IDs in configuration, and validate with a single conversations.history call per channel before building backfills.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.