Back to Blog
Guide

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.

Slack Green Team
October 2, 2026
October 2, 2026
4 min read
Share:
slack api
developers
channels

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

CallTokenResponse
conversations.join, public channelbotok, the bot is now a member
conversations.join, same channel againbotok, with "warning": "already_in_channel"
conversations.join, private channel the bot is not inbotchannel_not_found
conversations.join, private channel the bot is already inbotmethod_not_supported_for_channel_type
conversations.invite, public channel the bot is not inbotnot_in_channel
conversations.invite, a person already in the channelbotalready_in_channel
conversations.invite, the bot's own user IDbotcant_invite_self
conversations.invite, USLACKBOTbotcant_invite
conversations.invite, the user ID of a deleted app's botbotcant_invite
conversations.invite, another app's live botbotok
conversations.invite, a user ID that does not existbotuser_not_found
conversations.invite, our bot into a private channeluserok

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:

Slack web: sglabw1 APP, 10:24 AM,

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:

Slack web: sieun, 10:23 AM,

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.

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 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 Green Team
Guide

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 Green Team
Guide

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.

Slack Green Team