Automate Slack team onboarding by listening to the team_join Events API event, then calling conversations.invite to add the new user to default channels and chat.postMessage (or chat.postEphemeral for private delivery) to send a personalized welcome DM or channel message. Critical: your event handler must acknowledge team_join within 3 seconds — do the actual work asynchronously or Slack retries delivery repeatedly.
API Quick Reference
Bot Token (xoxb-)
1 msg/sec/channel (chat.postMessage); ~50 req/min (conversations.invite)
JSON
Available
Understanding the Slack API
Slack's onboarding automation combines the Events API (for receiving real-time notifications when events happen in a workspace) with the Web API (for taking actions like inviting users and sending messages). When a new member joins, Slack POSTs a team_join event to your configured endpoint. Your server must respond with HTTP 200 within 3 seconds — Bolt's ack() call handles this — then do the actual work asynchronously.
The two core actions are conversations.invite (to add the user to default channels like #general, #announcements, and team-specific channels) and chat.postMessage to send a personalized onboarding message. Use chat.postEphemeral if you want only the new member to see the welcome message in a channel rather than a visible public post.
The Events API delivers events to your public HTTPS endpoint. For development without a public URL, use Socket Mode with an App-Level Token (xapp-) — it opens a WebSocket to Slack instead. For production, use the HTTP Events API. Official docs: https://docs.slack.dev/apis/events-api
https://slack.com/apiSetting Up Slack API Authentication
Bot tokens (xoxb-) are workspace-scoped and long-lived by default. Team onboarding requires the team_join event subscription, chat:write scope for messaging, conversations:invite for channel additions, and users:read if you need to look up user details. The 3-second ack deadline on events is enforced strictly — always respond immediately and process asynchronously.
- 1Go to https://api.slack.com/apps and click Create New App > From scratch.
- 2Name your app and select your workspace.
- 3Click OAuth & Permissions in the left sidebar. Add Bot Token Scopes: chat:write, conversations:invite, channels:read, users:read, im:write (for DMs).
- 4Click Event Subscriptions in the left sidebar. Toggle Enable Events to On.
- 5Set the Request URL to your app's HTTPS endpoint (e.g., https://yourapp.com/slack/events). Slack will verify by sending a challenge.
- 6Under Subscribe to bot events, add: team_join.
- 7Click Install to Workspace and authorize. Copy the Bot User OAuth Token (xoxb-).
- 8Store SLACK_BOT_TOKEN and SLACK_SIGNING_SECRET in environment variables.
1import os2from slack_bolt import App3from slack_bolt.adapter.flask import SlackRequestHandler4from flask import Flask, request56# pip install slack-bolt flask7app = App(token=os.environ["SLACK_BOT_TOKEN"],8 signing_secret=os.environ["SLACK_SIGNING_SECRET"])910@app.event("team_join")11def handle_team_join(event, say, client, logger):12 user_id = event["user"]["id"]13 logger.info(f"New member joined: {user_id}")14 # Note: ack() is handled automatically by Bolt15 # Do work here or dispatch to async task1617flask_app = Flask(__name__)18handler = SlackRequestHandler(app)1920@flask_app.route("/slack/events", methods=["POST"])21def events():22 return handler.handle(request)2324if __name__ == "__main__":25 flask_app.run(port=3000)Security notes
- •Store SLACK_BOT_TOKEN and SLACK_SIGNING_SECRET in environment variables.
- •Always verify the Slack request signature (X-Slack-Signature header) before processing events — Bolt does this automatically.
- •Respond to the events endpoint within 3 seconds — use async processing for any work that takes longer.
- •Do not include the signing secret in client-side code or logs.
- •Use HTTPS for your events endpoint — Slack rejects HTTP URLs for event delivery.
Key endpoints
/conversations.inviteAdds one or more users to a channel. The bot must already be a member of the channel. Returns the updated channel object.
| Parameter | Type | Required | Description |
|---|---|---|---|
channel | string | required | Channel ID (C...) to invite the user to |
users | string | required | Comma-separated user IDs to invite (U...) |
Request
1{"channel":"C01234567","users":"U0987654"}Response
1{"ok":true,"channel":{"id":"C01234567","name":"general","is_member":true}}/chat.postMessageSends a message to a channel or opens a DM with the new user. Use the user ID as the channel parameter to open a DM. Supports Block Kit for formatted onboarding checklists.
| Parameter | Type | Required | Description |
|---|---|---|---|
channel | string | required | Channel ID or user ID (to open a DM) |
text | string | required | Fallback text for notifications — always include with blocks |
blocks | array | optional | Block Kit blocks for the formatted onboarding checklist |
Request
1{"channel":"U0987654","text":"Welcome to the team!","blocks":[{"type":"section","text":{"type":"mrkdwn","text":"*Welcome to Acme Inc!* :wave:\n\nHere's your onboarding checklist:\n- [ ] Read the handbook\n- [ ] Set up 2FA\n- [ ] Introduce yourself in #introductions"}}]}Response
1{"ok":true,"channel":"D01234567","ts":"1746612000.000100","message":{"type":"message","text":"Welcome to the team!"}}/chat.postEphemeralSends a message visible only to the specified user in a channel. Good for welcome messages in a public channel that only the new member sees, avoiding notification noise for everyone else.
| Parameter | Type | Required | Description |
|---|---|---|---|
channel | string | required | Channel ID where the ephemeral message appears |
user | string | required | User ID of the recipient — only they see the message |
Request
1{"channel":"C01234567","user":"U0987654","text":"Welcome! Only you can see this message."}Response
1{"ok":true,"message_ts":"1746612000.000200"}/conversations.listReturns a list of channels in the workspace — use to look up channel IDs by name. Cache the results rather than calling on every onboarding event.
| Parameter | Type | Required | Description |
|---|---|---|---|
types | string | optional | Comma-separated list of channel types: public_channel, private_channel, mpim, im |
limit | number | optional | Max channels per page (default 100, max 1000) |
Response
1{"ok":true,"channels":[{"id":"C01234567","name":"general"},{"id":"C02345678","name":"announcements"}],"response_metadata":{"next_cursor":""}}Step-by-step automation
Subscribe to the team_join Event
Why: team_join is the trigger that fires when a new member joins the workspace — all onboarding automation starts here.
In your app's Event Subscriptions settings, enable events and add team_join as a subscribed event. Slack verifies your endpoint by sending a url_verification challenge with a challenge field — your handler must echo it back as a plain JSON response. Once verified, Slack delivers team_join events to your endpoint whenever someone joins.
1# Handle Slack's URL verification challenge2# Your endpoint must respond with: {"challenge": "<value from request>"}3# Test your endpoint is reachable:4curl -X POST \5 -H "Content-Type: application/json" \6 -d '{"type":"url_verification","challenge":"test_challenge_123"}' \7 "https://yourapp.com/slack/events"Pro tip: The team_join event payload's user field is sometimes a full user object and sometimes just a user ID string — handle both cases by checking typeof user === 'object'.
Expected result: When a new user joins the workspace, the team_join handler fires with the user object. The event payload includes the user's ID, name, and profile data.
Invite the New User to Default Channels
Why: New members are automatically added to #general, but team-specific channels (like #engineering, #design, #random) require explicit invitations.
Call conversations.invite with the new user's ID and each channel ID you want them to join. Look up channel IDs once at startup using conversations.list and cache them. Invite one channel at a time (the users parameter accepts comma-separated IDs, but one failure fails all in one call). Handle already_in_channel responses gracefully — they are not errors.
1curl -X POST \2 -H "Authorization: Bearer $SLACK_BOT_TOKEN" \3 -H "Content-Type: application/json" \4 -d '{"channel":"C01234567","users":"U0987654"}' \5 "https://slack.com/api/conversations.invite"Pro tip: Fetch and cache default channel IDs at startup using conversations.list rather than hardcoding them. This makes it easy to update the onboarding channel list without redeploying.
Expected result: The new user appears in each default channel. already_in_channel responses for channels they already belong to are handled silently.
Send a Personalized Welcome DM
Why: A DM with an onboarding checklist is more actionable than a public channel message and doesn't create noise for existing team members.
Call chat.postMessage with the user's ID as the channel parameter — Slack automatically opens a DM. Use Block Kit to create a structured onboarding checklist with sections for each action the new member should take. Include relevant links to your handbook, tools, and introductory resources. The message appears in the new member's direct messages from your bot.
1curl -X POST \2 -H "Authorization: Bearer $SLACK_BOT_TOKEN" \3 -H "Content-Type: application/json" \4 -d '{5 "channel": "U0987654",6 "text": "Welcome to the team!",7 "blocks": [8 {"type":"section","text":{"type":"mrkdwn","text":"*Welcome to Acme Inc, <@U0987654>!* :wave:"}},9 {"type":"section","text":{"type":"mrkdwn","text":"*Your onboarding checklist:*\n:white_square_button: Read the <https://handbook.acme.com|Employee Handbook>\n:white_square_button: Set up <https://mfa.acme.com|2FA> on your accounts\n:white_square_button: Introduce yourself in <#C04444444>\n:white_square_button: Book your first <https://calendly.com/manager|1:1 with your manager>"}}10 ]11 }' \12 "https://slack.com/api/chat.postMessage"Pro tip: Use chat.postEphemeral in the #general channel instead of DM if your workspace culture prefers visible onboarding. Ephemeral messages are only visible to the new member but appear in a shared channel context.
Expected result: The new member receives a DM from the bot with a formatted onboarding checklist. The message appears in their Direct Messages section.
Handle the 3-Second Ack Deadline
Why: If your event handler takes more than 3 seconds to respond, Slack considers it failed and retries delivery — causing duplicate onboarding messages.
Slack's Events API requires your endpoint to return HTTP 200 within 3 seconds. For operations that take longer (looking up multiple channels, sending multiple messages), respond immediately and process asynchronously. With Bolt, ack() is called before your handler code. For raw HTTP, respond with 200 immediately, then process in a background thread.
1# Your endpoint should respond 200 immediately2# Test response time:3time curl -s -o /dev/null -w '%{time_total}' -X POST \4 -H "Content-Type: application/json" \5 -d '{"type":"event_callback","event":{"type":"team_join","user":{"id":"U123"}}}' \6 "https://yourapp.com/slack/events"7# Target: < 1.0 secondsPro tip: Use @slack/bolt (Node.js) or slack-bolt (Python) — these frameworks handle ack() automatically and run your handler code after the response is sent. Manual threading/async is error-prone.
Expected result: Your endpoint responds with HTTP 200 within 1 second. Onboarding actions (channel invites, DM) run in the background and complete within 5-10 seconds without affecting the Slack delivery acknowledgment.
Complete working code
A complete Slack onboarding bot built with slack-bolt and Flask. On team_join, it immediately acks the event, then invites the new user to configured default channels and sends a personalized DM with an onboarding checklist.
1import os, logging2from slack_bolt import App3from slack_bolt.adapter.flask import SlackRequestHandler4from flask import Flask, request as flask_req56logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")78app = App(9 token=os.environ["SLACK_BOT_TOKEN"],10 signing_secret=os.environ["SLACK_SIGNING_SECRET"]11)1213# Configure your default channels here (use IDs, not names)14DEFAULT_CHANNELS = [15 os.environ.get("SLACK_CHANNEL_GENERAL", ""),16 os.environ.get("SLACK_CHANNEL_TEAM", ""),17 os.environ.get("SLACK_CHANNEL_RANDOM", "")18]19DEFAULT_CHANNELS = [c for c in DEFAULT_CHANNELS if c] # Remove empty2021def invite_user(client, user_id: str) -> None:22 for channel_id in DEFAULT_CHANNELS:23 resp = client.conversations_invite(channel=channel_id, users=user_id)24 if not resp["ok"] and resp.get("error") != "already_in_channel":25 logging.warning(f"Invite to {channel_id} failed: {resp.get('error')}")2627def send_onboarding_dm(client, user_id: str) -> None:28 blocks = [29 {"type": "section", "text": {"type": "mrkdwn",30 "text": f"*Welcome to the team, <@{user_id}>!* :tada:"}},31 {"type": "divider"},32 {"type": "section", "text": {"type": "mrkdwn", "text":33 "*Your onboarding checklist:*\n"34 ":white_square_button: Read the <https://handbook.example.com|Handbook>\n"35 ":white_square_button: Set up 2FA on your accounts\n"36 ":white_square_button: Introduce yourself in your team channel\n"37 ":white_square_button: Schedule your first 1:1 with your manager"}},38 {"type": "context", "elements": [{"type": "mrkdwn",39 "text": "Questions? Post in #help-desk or DM @hr-bot"}]}40 ]41 resp = client.chat_postMessage(42 channel=user_id,43 text="Welcome to the team!",44 blocks=blocks45 )46 if not resp["ok"]:47 logging.error(f"Failed to DM {user_id}: {resp.get('error')}")4849@app.event("team_join")50def handle_team_join(event, client, logger):51 user = event.get("user", {})52 user_id = user.get("id") if isinstance(user, dict) else user53 if not user_id:54 logger.warning("team_join event missing user ID")55 return56 logger.info(f"Onboarding new member: {user_id}")57 invite_user(client, user_id)58 send_onboarding_dm(client, user_id)59 logger.info(f"Onboarding complete for: {user_id}")6061flask_app = Flask(__name__)62handler = SlackRequestHandler(app)6364@flask_app.route("/slack/events", methods=["POST"])65def events():66 return handler.handle(flask_req)6768if __name__ == "__main__":69 flask_app.run(port=int(os.environ.get("PORT", 3000)))70Error handling
HTTP 429, body: {"ok":false,"error":"ratelimited"}chat.postMessage exceeded 1 msg/sec/channel, or conversations.invite exceeded its Tier 3 rate limit (~50 req/min). Note: rate limits are per method, per workspace, per minute.
Read the Retry-After header and sleep for that duration plus 0.5 seconds. Add 100ms delays between sequential conversations.invite calls. For chat.postMessage, ensure you're not sending to the same channel more than once per second.
Sleep for Retry-After + 0.5 seconds, then retry once.
{"ok":false,"error":"already_in_channel"}The user is already a member of the channel — common if re-processing an event or if they were previously added manually.
Treat already_in_channel as a success — ignore it and continue with the next channel. This is an expected non-error state.
Do not retry — skip to the next channel.
{"ok":false,"error":"not_authed"}The Authorization header is missing or the bot token is invalid/revoked.
Verify the token is xoxb- prefixed. Reinstall the app to generate a fresh token. Store SLACK_BOT_TOKEN in environment variables and load it at startup.
Do not retry — fix the token.
{"ok":false,"error":"missing_scope","needed":"conversations:invite"}The bot token lacks the conversations:invite scope required to invite users to channels.
Add conversations:invite to OAuth & Permissions > Bot Token Scopes and reinstall the app to get a token with the updated scopes.
Do not retry — add the scope and reinstall.
Rate Limits for Slack API
| Scope | Limit | Window |
|---|---|---|
| chat.postMessage (per channel) | 1 message | per second |
| conversations.invite (Tier 3) | ~50 requests | per minute |
| Events API delivery | 30,000 deliveries | per workspace per hour |
1import time23def slack_call_with_retry(client_method, max_retries=5, **kwargs):4 for i in range(max_retries):5 try:6 return client_method(**kwargs)7 except Exception as e:8 if hasattr(e, 'response') and e.response.get('error') == 'ratelimited':9 retry_after = int(e.response.headers.get('Retry-After', 2))10 time.sleep(retry_after + 0.5)11 continue12 raise13 raise RuntimeError('Max retries exceeded')- Acknowledge team_join events within 3 seconds — always use Bolt or dispatch onboarding logic to a background thread.
- Add 100ms delays between sequential conversations.invite calls to stay well under the Tier 3 limit.
- Cache channel IDs at startup using conversations.list — avoid re-fetching on every onboarding event.
- Implement idempotency: log user IDs that have been onboarded and skip re-onboarding if the event is replayed.
- For large team migrations (100+ simultaneous joins), use a queue system to rate-limit onboarding tasks rather than processing all events simultaneously.
Security checklist
- Store SLACK_BOT_TOKEN and SLACK_SIGNING_SECRET in environment variables — never hardcode.
- Verify the X-Slack-Signature on every incoming event request before processing — prevents replay attacks.
- Request only the scopes you need: chat:write, conversations:invite, users:read, im:write.
- Use HTTPS for your Events API endpoint — Slack rejects HTTP URLs.
- Implement idempotency: store onboarded user IDs to prevent duplicate onboarding if Slack retries event delivery.
- Don't include sensitive company information (salary bands, HR notes) in automated onboarding messages.
Automation use cases
Role-Based Channel Assignment
intermediateInvite users to role-specific channels (e.g., #engineering, #design, #sales) based on their profile's title or department field.
Manager Introduction
advancedLook up the new member's manager via HR API and automatically send an introduction message in a shared channel.
Onboarding Task Checklist
advancedSend an interactive Block Kit checklist where each item has a checkbox button — track completion state in a database.
Welcome Video DM
beginnerSend a DM with a Loom or YouTube welcome video from the CEO or team lead as the first touchpoint.
No-code alternatives
Don't want to write code? These platforms can automate the same workflows visually.
Zapier
Free tier availableZapier has a native Slack trigger for New Team Member — can automatically send messages and invite to channels without code.
- + Native team_join trigger
- + No code required
- + Easy channel invite actions
- - Limited Block Kit customization
- - Per-task pricing adds up at scale
- - No idempotency logic built in
Make (formerly Integromat)
Free tier available (1,000 operations/month)Make's Slack module includes a New Member trigger and can send messages with full JSON control over Block Kit payloads.
- + More affordable than Zapier
- + Full Block Kit JSON support via webhook module
- + Visual workflow builder
- - Steeper learning curve
- - team_join trigger timing can be delayed
- - Execution limits on free tier
n8n
Free self-hosted; Cloud from €20/monthn8n's Slack trigger supports team_join events and its HTTP node gives full API access for channel invites and custom onboarding flows.
- + Self-hostable
- + Full Slack API access
- + Flexible workflow logic
- - Requires self-hosting for production
- - More setup than Zapier
- - No visual preview of Slack messages
Best practices
- Always respond to Slack event deliveries within 3 seconds — use Bolt which handles ack() automatically, or use Flask with background threads.
- Implement idempotency: track which users have been onboarded in a database. Slack can deliver the same event multiple times if your endpoint is slow.
- Cache channel IDs at startup with conversations.list — never fetch them on every onboarding event.
- Use user IDs (U...) as the channel in chat.postMessage to open DMs — do not try to find or open the DM channel ID manually.
- Handle already_in_channel responses gracefully — they are expected when users join channels they were previously in.
- Test the full flow by creating a test user in a dev workspace before deploying to production.
- For enterprise Slack workspaces, check if your app has org-wide install vs. workspace-install and whether team_join events are scoped to the workspace or org level.
Ask AI to help
Copy one of these prompts to get a personalized, working implementation.
I'm building a Slack onboarding bot using Python and slack-bolt. When a new member joins (team_join event), I need to: 1) acknowledge the event within 3 seconds (Bolt handles this automatically), 2) invite the user to 3 default channels using conversations.invite, 3) send a personalized DM with a Block Kit onboarding checklist using chat.postMessage with the user ID as the channel, and 4) handle already_in_channel errors gracefully. Show me the complete implementation including Bolt app setup, Flask route, and error handling.
Build a Slack onboarding dashboard. Features: a list of recently joined workspace members with their join date and onboarding status (invited to channels, DM sent), a configuration panel to set which channels new members are invited to (with channel name lookup), a customizable onboarding message editor with a live Block Kit preview, re-send onboarding button for missed users, and a template library for different team roles (engineering, design, sales) with different channel configurations and welcome messages.
Frequently asked questions
Is the Slack API free for onboarding automation?
Yes. Creating a Slack app, subscribing to events, and calling Web API methods (chat.postMessage, conversations.invite) are all free. There are no per-event or per-message fees. Rate limits apply regardless of plan.
What happens if I don't acknowledge the team_join event within 3 seconds?
Slack considers the delivery failed and retries — eventually sending the event multiple times. This causes duplicate onboarding messages and duplicate channel invites. Always use Bolt (which acks automatically) or respond to the HTTP request with 200 immediately and process asynchronously.
Why does conversations.invite fail with 'not_in_channel'?
This error means the bot is not a member of the channel it's trying to invite the user into. Invite the bot first: in Slack, type /invite @YourBotName in the channel. The bot must be in a channel before it can invite others to it.
Can I send to multiple Slack workspaces with one bot token?
No. Each bot token (xoxb-) is workspace-scoped. For multi-workspace deployments, implement the OAuth install flow to collect tokens per workspace and store them in a database keyed by workspace ID.
What is chat.postEphemeral and when should I use it for onboarding?
chat.postEphemeral sends a message visible only to the specified user in a public channel. Use it for welcome messages in #general if you want the new member to see it in context without sending a notification to everyone else. The downside is ephemeral messages disappear after the user reloads Slack — not ideal for checklists they need to reference later.
How do I handle the team_join event if my server is temporarily down?
Slack retries failed event deliveries with exponential backoff for up to 24 hours. Ensure your server handles duplicate events (same user_id) by checking if that user has already been onboarded in your database before processing.
Can RapidDev help build a custom Slack onboarding system?
Yes. RapidDev has built 600+ apps including Slack bots with role-based channel routing, HR system integrations, and interactive onboarding checklists. 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