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.
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 API Pagination: next_cursor, the Last Page and invalid_cursor, Tested
Slack paginates list methods with a cursor: pass response_metadata.next_cursor back as cursor until it is empty. We paged 27 real messages on 2 October 2026 and broke the cursor five ways to see which ones fail.
Slack RTM API Deprecated: What rtm.connect Returns for a New App, and the Socket Mode Fix
A Slack app created today cannot use the RTM API. We called rtm.connect and rtm.start with every token a new app gets on 2 October 2026, tried to request the rtm:stream scope, and ran the same bot over Socket Mode.
Slack Bot Icon and Name Per Message: icon_emoji, icon_url and username, Tested
icon_emoji, icon_url and username only work with the chat:write.customize scope, and Slack ignores them silently without it. We tested every case on 2 October 2026, plus webhooks and the app icon upload limits.