Slack API: Add a User to a Channel (conversations.invite), Tested
We added people and bots to public and private channels with conversations.invite and conversations.join, using a bot token and a user token. Every response we got, including already_in_channel, cant_invite_self, cant_invite and the force flag.
On this page
To add a user to a Slack channel with the API, call conversations.invite with the channel ID and a comma-separated list of user IDs in users. The token's owner must already be in the channel, and the token needs channels:manage for public channels or groups:write for private ones (bot scopes; a user token uses channels:write and groups:write). For a bot to put itself into a public channel, use conversations.join with channels:join. We ran every call below on 2 October 2026 in our own test workspace, with a bot token and with a user token.
Add a user with Python
This script, run with slack_sdk 3.45.0, covers the common cases. PUBLIC_ID and PRIVATE_ID were two new channels a person had created, so the bot was in neither:
import os
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
bot = WebClient(token=os.environ["SLACK_BOT_TOKEN"])
ME = os.environ["MY_USER_ID"] # the person to add
PUBLIC, PRIVATE = os.environ["PUBLIC_ID"], os.environ["PRIVATE_ID"]
def run(label, fn, **kw):
try:
r = fn(**kw)
print(f"{label:42} ok warning={r.get('warning')}")
except SlackApiError as e:
errs = [(x["user"], x["error"]) for x in e.response.get("errors", [])]
print(f"{label:42} {e.response['error']} {errs or ''}")
new = bot.conversations_create(name="w1-invite-demo-1002", is_private=True)["channel"]["id"]
run("join a public channel", bot.conversations_join, channel=PUBLIC)
run("join a private channel", bot.conversations_join, channel=PRIVATE)
run("invite a person to the bot's channel", bot.conversations_invite, channel=new, users=ME)
run("invite the same person again", bot.conversations_invite, channel=new, users=ME)
run("invite the bot itself", bot.conversations_invite, channel=new, users=bot.auth_test()["user_id"])
run("invite into a private channel it is not in", bot.conversations_invite, channel=PRIVATE, users=ME)
print("created", new)
Output:
join a public channel ok warning=None
join a private channel channel_not_found
invite a person to the bot's channel ok warning=None
invite the same person again already_in_channel [('U0B7L4YK420', 'already_in_channel')]
invite the bot itself cant_invite_self [('U0C695L6YVA', 'cant_invite_self')]
invite into a private channel it is not in channel_not_found
created C0C6980KWA0
The bot can invite people into the private channel it created, because it is a member. It cannot see the private channel a person created, so both conversations.join and conversations.invite say channel_not_found, not a permissions error. That is the same error a wrong channel ID gives; our channel_not_found page lists the other causes.
Every response we got
| Call | Token | Response |
|---|---|---|
conversations.join, public channel | bot | ok, the bot is now a member |
conversations.join, same channel again | bot | ok, with "warning": "already_in_channel" |
conversations.join, private channel the bot is not in | bot | channel_not_found |
conversations.join, private channel the bot is already in | bot | method_not_supported_for_channel_type |
conversations.invite, public channel the bot is not in | bot | not_in_channel |
conversations.invite, a person already in the channel | bot | already_in_channel |
conversations.invite, the bot's own user ID | bot | cant_invite_self |
conversations.invite, USLACKBOT | bot | cant_invite |
conversations.invite, the user ID of a deleted app's bot | bot | cant_invite |
conversations.invite, another app's live bot | bot | ok |
conversations.invite, a user ID that does not exist | bot | user_not_found |
conversations.invite, our bot into a private channel | user | ok |
Two of these behave unlike the rest. A second conversations.join is not an error: Slack returns ok: true and puts already_in_channel in warning, so code that only checks ok will not notice. And conversations.join never works on a private channel, even when the bot is already in it. To see who is in a channel before you invite, call conversations.members; get users in a channel shows what it returns for each channel type.
Invite errors come with an errors array that names the user each error belongs to:
{"ok": false, "error": "cant_invite_self", "errors": [{"user": "U0C695L6YVA", "ok": false, "error": "cant_invite_self"}]}
Never appear "away" on Slack again
Cloud-based. No downloads. Works 24/7 even when your laptop is off.
Several users at once, and force
users takes a list of IDs separated by commas (Slack's docs allow up to 1,000). If one of them fails, the whole call fails by default. We invited a made-up ID and a real person to a new private channel:
{"ok": false, "error": "user_not_found", "errors": [{"user": "U0NOTREAL1", "ok": false, "error": "user_not_found"}]}
conversations.members afterwards listed only the bot, so the real person was not added either. The same call with force=true returned ok: true with the same errors array, and the person was in the channel. Use force when a list may contain IDs of people who left; check errors after the call to see who was skipped.
Adding a bot to a channel
A bot cannot join a private channel by itself. A member has to invite it, in Slack with /invite @botname or through the API with that member's user token. With our user token (groups:write user scope), conversations.invite with the bot's user ID returned ok for a private channel the person had created. After that, the bot was a member: its conversations.leave in that channel returned ok. If the bot only needs to post, not read, the not_in_channel page covers chat:write.public as another way.
Each join or invite leaves a system message in the channel. Slack groups them. When our bot created a channel and invited a person and another app's bot, the channel showed one line:
When a person invited the bot to a private channel and the bot then left with conversations.leave, the two events merged into the person's join line:
In conversations.history each of these is a separate message with subtype channel_join or channel_leave. A join made by conversations.invite carries an inviter field with the ID of whoever made the call; a conversations.join has none.
FAQ
Can a bot rejoin a private channel after it leaves?
No. After our bot left the private channel with conversations.leave, its conversations.join returned channel_not_found. Someone in the channel has to invite it again.
How do I find the channel ID to pass?
Use conversations.list or copy it from the channel details in Slack. Both ways are in how to find a Slack channel ID.
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
Slack Bot Scopes: The Scope Each API Method Needs, Measured on 44 Methods
We gave a Slack bot one scope, called 44 common Web API methods, and read the needed field of every missing_scope error. Then we added the scopes and called them again. The table shows which scope clears each method.
Slack File Size Limit: 1 GB Is Exactly 1,073,741,824 Bytes, Tested
We uploaded files of 50 MB, exactly 1 GiB and 1 GiB plus one byte to Slack through its upload API. The limit is 1,073,741,824 bytes, and a failed upload can still leave a broken file behind.
Slack Dark Mode: Where the Setting Is, and Which Custom Emoji Disappear
Slack's dark mode is under Preferences > Appearance > Color Mode, with Light, Dark and System. We switched modes in Slack web with three test emoji and measured which ones vanish.