Automate Discord welcome messages by listening to the GUILD_MEMBER_ADD Gateway event (requires GUILD_MEMBERS privileged intent enabled in both Developer Portal and code) and posting an embed via POST /channels/{channel.id}/messages. This is Gateway-only — there is no REST polling fallback. The bot must maintain an active WebSocket connection. Rate limit: 5 messages per 5 seconds per channel.
API Quick Reference
Bot Token
5 messages / 5 seconds per channel
JSON
Available
Understanding the Discord API
Discord's architecture separates real-time events (Gateway WebSocket) from actions (REST API). Welcome messages are driven entirely by the Gateway: when a user joins, Discord fires a GUILD_MEMBER_ADD event to all bots connected to that guild via WebSocket. There is no REST endpoint to poll for new joins — the Gateway connection is mandatory.
The GUILD_MEMBERS privileged intent is required to receive GUILD_MEMBER_ADD. Without it, the Gateway silently drops the event (close code 4014 if you explicitly request the intent without enabling it in the portal). For bots in fewer than 100 guilds, toggle it on in the Developer Portal. For bots in 100+ guilds, Discord requires verification plus staff review.
Once the event arrives, sending the welcome message is a standard REST call: POST /channels/{channel.id}/messages with an embed payload. The channel ID is your designated welcome channel, which you configure in your bot's settings. Official docs: https://discord.com/developers/docs/events/gateway-events#guild-member-add
https://discord.com/api/v10Setting Up Discord API Authentication
Bot tokens are static credentials that do not expire until reset. You must also enable the GUILD_MEMBERS privileged intent in two places: the Developer Portal toggle AND your code's intent bitmask. Mismatching these causes Gateway close code 4014 — the bot connects but receives no member events.
- 1Go to https://discord.com/developers/applications and click New Application.
- 2Open the Bot tab. Click Reset Token — copy it immediately (shown once).
- 3Under Privileged Gateway Intents, toggle on Server Members Intent.
- 4Open OAuth2 > URL Generator. Select scopes: bot and applications.commands.
- 5Under Bot Permissions, check: Send Messages, Embed Links, View Channels.
- 6Copy the generated URL and invite the bot to your test server.
- 7Create or identify your welcome channel and copy its ID (right-click channel > Copy Channel ID).
- 8Store DISCORD_BOT_TOKEN and DISCORD_WELCOME_CHANNEL_ID in environment variables.
1import os2import discord34# discord.py v2.7.1 requires Python 3.8+5# Install: pip install discord.py67BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"]8WELCOME_CHANNEL_ID = int(os.environ["DISCORD_WELCOME_CHANNEL_ID"])910# Enable GUILD_MEMBERS intent in code — must match Developer Portal setting11intents = discord.Intents.default()12intents.members = True # Enables GUILD_MEMBER_ADD events1314client = discord.Client(intents=intents)1516@client.event17async def on_ready():18 print(f"Bot connected as {client.user}")1920client.run(BOT_TOKEN)Security notes
- •Store DISCORD_BOT_TOKEN in environment variables — never commit it to version control.
- •Enable only the privileged intents you actually need — Server Members for GUILD_MEMBER_ADD, nothing else for this use case.
- •Validate that the welcome channel ID exists and the bot has Send Messages and View Channel permissions before going live.
- •If the token is exposed, immediately click Reset Token in the Developer Portal.
- •Use a test server during development to avoid spamming your production server's welcome channel.
Key endpoints
/channels/{channel.id}/messagesSends a message to the welcome channel. Include content for a text ping and embeds for a formatted welcome card. This is the only REST endpoint needed for welcome message automation.
| Parameter | Type | Required | Description |
|---|---|---|---|
content | string | optional | Text content — mention the user here to trigger a notification ping |
embeds | array | optional | Up to 10 embed objects. Use one embed for the welcome card with title, description, color, and thumbnail. |
Request
1{"content":"<@123456789>","embeds":[{"title":"Welcome to the Server!","description":"We're glad you're here, <@123456789>! Read the rules in #rules and introduce yourself in #introductions.","color":5814783,"thumbnail":{"url":"https://cdn.discordapp.com/avatars/123456789/avatar_hash.png"},"footer":{"text":"Member #42"}}]}Response
1{"id":"998877665544","type":0,"content":"<@123456789>","channel_id":"111222333444","author":{"id":"bot_id","username":"WelcomeBot","bot":true},"embeds":[{"title":"Welcome to the Server!","description":"We're glad you're here!","color":5814783}],"timestamp":"2026-05-07T10:00:00.000Z"}/guilds/{guild.id}/membersFetches a list of guild members — use this to get the total member count for displaying 'Member #N' in the welcome message. Requires GUILD_MEMBERS privileged intent.
| Parameter | Type | Required | Description |
|---|---|---|---|
guild.id | string | required | The snowflake ID of the guild |
limit | number | optional | Max members to return (1-1000, default 1) |
Response
1[{"user":{"id":"123456789","username":"johndoe","avatar":"abc123"},"roles":["role_id"],"joined_at":"2026-05-07T10:00:00.000Z","deaf":false,"mute":false}]/guilds/{guild.id}?with_counts=trueReturns guild info including approximate_member_count and approximate_presence_count — use this for member count in welcome messages without needing to fetch all members.
| Parameter | Type | Required | Description |
|---|---|---|---|
with_counts | boolean | optional | Set to true to include approximate_member_count in the response |
Response
1{"id":"555555555555","name":"My Server","approximate_member_count":1247,"approximate_presence_count":423}Step-by-step automation
Enable GUILD_MEMBERS Intent in Both Portal and Code
Why: Mismatching the intent — enabled in portal but not code, or vice versa — causes either silent event drops or close code 4014.
In the Discord Developer Portal, go to your application > Bot > Privileged Gateway Intents and toggle on Server Members Intent. In your code, set intents.members = True (discord.py) or add GatewayIntentBits.GuildMembers to your intents bitmask (discord.js). Both must match or GUILD_MEMBER_ADD events will never arrive.
1# Verify GUILD_MEMBER_ADD arrives by checking bot flags2curl -s -H "Authorization: Bot $DISCORD_BOT_TOKEN" \3 "https://discord.com/api/v10/users/@me" | python3 -m json.toolPro tip: Add a startup log message: print('Intents.members enabled:', client.intents.members) to confirm the intent is active before going to production.
Expected result: The on_member_join (Python) or guildMemberAdd (discord.js) event fires when a user joins the server. If it never fires, the intent is misconfigured.
Build the Welcome Embed
Why: A formatted embed is significantly more engaging than plain text and allows you to include the user's avatar, server rules link, and member number.
Construct an embed object with the new member's username, avatar URL (from the member.user.avatar field), and any server-specific information. The embed color, thumbnail, and footer fields are all optional but make the welcome message feel polished. Keep the description under 4096 characters.
1# Send a welcome embed directly2curl -X POST \3 -H "Authorization: Bot $DISCORD_BOT_TOKEN" \4 -H "Content-Type: application/json" \5 -d "{6 \"content\": \"<@$USER_ID>\",7 \"embeds\": [{8 \"title\": \"Welcome to the server!\",9 \"description\": \"We're happy you're here, <@$USER_ID>! Check out #rules and introduce yourself.\",10 \"color\": 5814783,11 \"thumbnail\": {\"url\": \"$AVATAR_URL\"},12 \"footer\": {\"text\": \"Member #1247\"}13 }]14 }" \15 "https://discord.com/api/v10/channels/$WELCOME_CHANNEL_ID/messages"Pro tip: Use Discord's embed preview tool (discohook.org) to prototype your embed layout before coding it — it shows exactly what members will see.
Expected result: An embed object ready to be passed to the message POST endpoint — formatted with the user's avatar, a description, and footer.
Send the Welcome Message on Member Join
Why: This is the core action — posting the embed to the welcome channel when the GUILD_MEMBER_ADD event fires.
In your GUILD_MEMBER_ADD handler, call POST /channels/{welcome_channel_id}/messages with the embed payload. In discord.py or discord.js, use the channel.send() method which handles the REST call internally. In raw REST, POST the message directly. Mention the user in the content field (not just the embed description) to trigger a notification ping.
1# This step is triggered by a Gateway event — no cURL equivalent2# For testing, manually send a welcome message:3curl -X POST \4 -H "Authorization: Bot $DISCORD_BOT_TOKEN" \5 -H "Content-Type: application/json" \6 -d '{"content":"<@USER_ID> Welcome!","embeds":[{"title":"Welcome!","color":5765632}]}' \7 "https://discord.com/api/v10/channels/$WELCOME_CHANNEL_ID/messages"Pro tip: During large join raids (e.g., after a Twitter shoutout), GUILD_MEMBER_ADD events queue up. Add a small random delay (50-500ms) before posting to avoid sending 50 messages/second to the welcome channel and hitting the per-channel rate limit.
Expected result: When a user joins the server, the welcome message appears in the designated channel within milliseconds of the join event.
Handle Join Raids with Rate Limit Awareness
Why: Discord's 5 messages per 5 seconds per channel limit means a raid with 100 joins in 10 seconds will back up your welcome message queue.
Implement an asyncio queue (Python) or a Promise queue (Node.js) that buffers GUILD_MEMBER_ADD events and processes them at a controlled rate. Track the channel rate limit bucket by reading X-RateLimit-Remaining on raw REST calls. For SDK-based bots, discord.js and discord.py handle some rate limiting automatically, but their queues don't throttle application-level spam.
1# Check rate limit headers after posting2curl -v -X POST \3 -H "Authorization: Bot $DISCORD_BOT_TOKEN" \4 -H "Content-Type: application/json" \5 -d '{"content":"Test"}' \6 "https://discord.com/api/v10/channels/$WELCOME_CHANNEL_ID/messages" \7 2>&1 | grep -i 'x-ratelimit'Pro tip: During an obvious join raid (50+ joins in 60 seconds), consider skipping individual welcome messages and posting a single message like 'Welcome to our 50 newest members!' to reduce noise.
Expected result: Welcome messages are sent one per second regardless of join rate, preventing channel rate limit exhaustion during raids.
Complete working code
A complete discord.py bot that listens for GUILD_MEMBER_ADD events, builds a formatted welcome embed with the user's avatar and member count, and posts it to a designated welcome channel with rate-limit-safe queuing. Runs continuously as a process.
1import os, asyncio, logging2from collections import deque3import discord45logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")67BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"]8WELCOME_CHANNEL_ID = int(os.environ["DISCORD_WELCOME_CHANNEL_ID"])910intents = discord.Intents.default()11intents.members = True # Required for GUILD_MEMBER_ADD1213client = discord.Client(intents=intents)14welcome_queue = deque()1516def build_embed(member: discord.Member) -> discord.Embed:17 embed = discord.Embed(18 title=f"Welcome to {member.guild.name}!",19 description=(20 f"{member.mention}, we're glad you joined!\n\n"21 f"Read the rules and get started."22 ),23 color=discord.Color.green()24 )25 embed.set_thumbnail(url=member.display_avatar.url)26 embed.set_footer(text=f"Member #{member.guild.member_count}")27 embed.timestamp = discord.utils.utcnow()28 return embed2930async def queue_processor():31 await client.wait_until_ready()32 channel = client.get_channel(WELCOME_CHANNEL_ID)33 if channel is None:34 logging.error(f"Welcome channel {WELCOME_CHANNEL_ID} not found")35 return36 logging.info(f"Welcome queue processor started, posting to #{channel.name}")37 while not client.is_closed():38 if welcome_queue:39 member = welcome_queue.popleft()40 try:41 await channel.send(content=member.mention, embed=build_embed(member))42 logging.info(f"Welcomed {member.name} (guild: {member.guild.name})")43 except discord.HTTPException as e:44 logging.error(f"Failed to welcome {member.name}: {e}")45 await asyncio.sleep(1.0) # Max 1 welcome/second — stays under rate limit4647@client.event48async def on_ready():49 logging.info(f"Connected as {client.user}")50 client.loop.create_task(queue_processor())5152@client.event53async def on_member_join(member: discord.Member):54 logging.info(f"{member.name} joined — queued welcome")55 welcome_queue.append(member)5657client.run(BOT_TOKEN)58Error handling
Disallowed intents specified — you requested an intent you have not been grantedThe GUILD_MEMBERS privileged intent is enabled in code but not toggled on in the Developer Portal, or the bot needs verification (100+ guilds) but hasn't been verified.
Go to https://discord.com/developers/applications > Bot tab > Privileged Gateway Intents and enable Server Members Intent. For bots in 100+ guilds, complete the verification process first.
Do not retry — fix the portal configuration. The Gateway will reject the connection until resolved.
{"message": "You are being rate limited.", "retry_after": 1.0}The welcome channel received more than 5 messages in 5 seconds — typically caused by a join raid without message queuing.
Implement a message queue that processes one welcome per second. The SDK (discord.py/discord.js) will queue messages automatically, but you should still add application-level throttling for large join spikes.
Discord SDKs handle this automatically with exponential backoff. For raw REST, sleep for retry_after + 0.2s before retrying.
{"code": 50013, "message": "Missing Permissions"}The bot does not have Send Messages or Embed Links permission in the welcome channel.
Check the welcome channel's permission overrides. Ensure the bot's role has Send Messages and Embed Links. If the channel inherits from a category, check category-level permissions too.
Do not retry — fix channel permissions.
{"code": 10003, "message": "Unknown Channel"}The DISCORD_WELCOME_CHANNEL_ID environment variable points to a channel that doesn't exist or has been deleted.
Re-copy the channel ID (right-click channel > Copy Channel ID in Discord). Verify the ID in your environment variables. Add a startup check: fetch the channel at on_ready and log an error if it returns null.
Do not retry — fix the channel ID configuration.
Rate Limits for Discord API
| Scope | Limit | Window |
|---|---|---|
| Per channel (messages) | 5 messages | per 5 seconds |
| Global (per bot) | 50 requests | per second |
| Gateway events | 120 events | per 60 seconds (Gateway send limit) |
1import asyncio2from collections import deque34class WelcomeQueue:5 def __init__(self, rate=1.0):6 self.queue = deque()7 self.rate = rate # messages per second89 async def add(self, member):10 self.queue.append(member)1112 async def process(self, channel, send_func):13 while True:14 if self.queue:15 member = self.queue.popleft()16 try:17 await send_func(channel, member)18 except Exception as e:19 print(f'Error: {e}')20 await asyncio.sleep(1.0 / self.rate)- Process welcome messages through a queue at 1/second to stay well under the 5/5s per-channel limit.
- Cache the welcome channel object at startup — calling client.get_channel() on every event is free (in-memory cache), but fetching from the API on every join is wasteful.
- Monitor join rate: if joins exceed 60/minute for 3+ minutes, switch to a batched welcome ('Welcome to our 20 newest members: @user1, @user2...') to reduce message volume.
- Store the welcome channel ID in config, not hardcoded — makes it easy to switch channels without redeploying.
- Test with a rate limit simulator: join the server from 10 alt accounts in quick succession to verify your queue handles the load correctly.
Security checklist
- Store DISCORD_BOT_TOKEN in environment variables — never in source code.
- Enable only the GUILD_MEMBERS privileged intent — not Message Content or Presence, unless your bot specifically needs them.
- Validate the welcome channel ID at startup and log an error if it resolves to null — prevents silent failures.
- Sanitize any user-provided content in the welcome message — do not include the username directly in a way that could render as a mention of a role or @everyone.
- Implement raid detection: if more than 20 members join in 60 seconds, log an alert and optionally pause welcome messages during bot-raid events.
- Use a test server during development to avoid polluting the production welcome channel with test events.
- Set up process monitoring (systemd, PM2, or a container) to auto-restart the bot if it crashes — GUILD_MEMBER_ADD events are lost during downtime.
Automation use cases
DM Welcome Message
beginnerSend a private DM to the new member with onboarding instructions and a server guide link, in addition to or instead of the public welcome channel message.
Auto-Role on Join
beginnerCombine welcome message with automatic role assignment — assign a default Member role immediately on GUILD_MEMBER_ADD.
Membership Tier Welcome
advancedCheck if the new member's email matches a Patreon or Stripe subscriber list and assign a tier-appropriate role and welcome message.
Multi-Language Welcome
intermediateDetect the new member's locale from their user data and send a welcome message in their language if supported.
No-code alternatives
Don't want to write code? These platforms can automate the same workflows visually.
Zapier
Free tier availableZapier's Discord integration cannot listen for member join events directly — it relies on triggers from other apps, making it unsuitable for pure join-based welcome messages.
- + Easy setup for cross-app triggers
- + No code required
- - Cannot listen to Discord Gateway events
- - Not suitable for join-based triggers
- - Requires a separate trigger source
Make (formerly Integromat)
Free tier available (1,000 operations/month)Make's Discord module has similar limitations to Zapier — it can post messages to Discord but cannot receive Gateway events like GUILD_MEMBER_ADD without a custom webhook bridge.
- + Can post formatted messages to Discord
- + Visual workflow builder
- - No native GUILD_MEMBER_ADD trigger
- - Requires custom webhook setup to receive join events
- - Added latency vs. direct bot
n8n
Free self-hosted; Cloud from €20/monthn8n can receive Discord webhooks and has a Discord trigger node, but GUILD_MEMBER_ADD specifically requires the bot to maintain a WebSocket connection — n8n's Discord integration covers webhook-based events better than Gateway events.
- + Self-hostable
- + More flexible than Zapier/Make
- + HTTP webhook support
- - GUILD_MEMBER_ADD is Gateway-only — requires custom bot bridge
- - Gateway events not natively supported
- - More complex setup
Best practices
- Enable GUILD_MEMBERS privileged intent in both the Developer Portal AND your code — mismatch causes silent failure or close code 4014.
- Always check that the welcome channel exists (not null) before trying to send — channel IDs can become invalid if the channel is deleted.
- Queue welcome messages and process at 1/second to handle join raids without hitting the 5 messages/5 seconds per-channel limit.
- Include the user's mention (not just username) in the content field outside the embed — this triggers the mobile push notification that welcome messages in embeds alone do not send.
- Cache the welcome channel object at startup using client.get_channel() rather than fetching it from the API on every join event.
- Use member.display_avatar.url (discord.py) or member.user.displayAvatarURL() (discord.js) for the thumbnail — these handle the default avatar fallback when the user has no custom avatar.
- Test by joining your test server from a second account — the actual join event is the only reliable way to verify the setup works end-to-end.
Ask AI to help
Copy one of these prompts to get a personalized, working implementation.
I'm building a Discord welcome message bot using discord.py v2.7.1. I've enabled the GUILD_MEMBERS privileged intent in both the Developer Portal and my code (intents.members = True). Help me: 1) handle the on_member_join event, 2) build a formatted embed with the member's avatar, username, and member count, 3) send it to a designated welcome channel, 4) implement a queue to process at max 1 welcome/second to avoid hitting the 5/5s rate limit during join raids, and 5) handle 403 Missing Permissions errors gracefully with a helpful log message.
Build a web dashboard for managing a Discord welcome message bot. It should have: a live preview of the welcome embed (showing title, description, color, thumbnail template), a form to configure the welcome channel ID and message template with variables like {username} and {member_count}, a test button that sends a preview to the welcome channel, a join analytics chart showing member growth over time, and a recent joins log showing the last 50 members with their join timestamps.
Frequently asked questions
Is the Discord API free to use for welcome bots?
Yes. The Discord REST API is completely free with no per-request pricing. The only costs are your server hosting. Privileged intents (including GUILD_MEMBERS) are also free — they require a portal toggle and verification at 100+ guilds, but there is no fee.
What happens if I hit the rate limit posting welcome messages?
You get HTTP 429 with retry_after in seconds. Discord's official libraries (discord.py, discord.js) handle this automatically with exponential backoff. However, during large join raids you may see welcome messages delayed by several seconds as the queue builds up. Implement a 1/second processing queue to prevent this.
Why does GUILD_MEMBER_ADD never fire on my bot?
Two causes: (1) the Server Members Intent is not enabled in the Developer Portal under your app's Bot tab, or (2) you enabled it in the portal but forgot intents.members = True in discord.py or GatewayIntentBits.GuildMembers in discord.js. Both portal and code must match. Also verify close code 4014 is not appearing in your bot's logs.
Can I send a welcome DM instead of posting to a channel?
Yes. In discord.py, use await member.send(embed=embed) instead of channel.send(). In discord.js, use member.user.send({ embeds: [embed] }). Note that some users have DMs disabled from non-friend users — wrap the send in a try/catch and fall back to the channel message if the DM fails.
Does my bot need to stay online 24/7 for welcome messages to work?
Yes. GUILD_MEMBER_ADD is a Gateway event delivered only to bots with an active WebSocket connection. If your bot is offline when a member joins, you will never receive that event — there is no replay or REST polling alternative. Host your bot on a always-on service (VPS, Railway, Fly.io, etc.).
Can RapidDev help build a custom Discord welcome bot?
Yes. RapidDev has built 600+ apps including Discord bots with welcome flows, role assignment, and CRM integrations. Visit rapidevelopers.com for a free consultation.
How do I include the server's total member count in the welcome message?
In discord.py, access member.guild.member_count — this is available directly from the GUILD_MEMBER_ADD event payload without an extra API call. In discord.js, use member.guild.memberCount. Note: this reflects the count after the new member joined, so the first member sees #1.
Can I trigger a welcome message from a REST API call without a running bot?
Partially. You can POST a message to the welcome channel via REST with just a bot token — no Gateway connection needed. But you won't know when to call it unless you have another system notifying you of joins. The Gateway (WebSocket) is the only reliable way to receive real-time join events.
Need this automated?
Our team has built 600+ apps with API automations. We can build this for you.
Book a free consultation