Discord birthday bot: greet every member on their day (Python and JavaScript)
Wishing everyone a happy birthday on their day is one of the things that brings a Discord community together, and one of the first to be forgotten. With this bot, each member saves their birthday once (without the year) and the bot greets them in the morning in the channel you choose, even if it was off at that time. Full code in discord.py and discord.js, no database and no privileged intents.
What the bot does
Four commands: /birthday to save your day and month, /forget-birthday to remove it, /birthdays to see the next 10 and /birthday-channel to choose where greetings go (by default, only people who manage the server see that one).
Every day, from 9:00 local time, the bot writes in that channel “🎂 Happy birthday, @Anna and @Luke! 🎉”, mentioning each person so they get the notification. It saves which day it has already greeted, so a restart doesn't greet twice, and if it was off at 9 it greets as soon as it's back on that same day.
It doesn't save the year: nobody has to say how old they are. Someone born on 29 February gets greeted on the 28th in years that aren't leap years. If someone has left the server, the bot doesn't name them. Everything goes in birthdays.json, next to the bot, and one bot can handle the birthdays of several servers at once.
Rather not set it up yourself? Discord bot hosting from €1.50 a month, no contract and a full refund within 72 hours. Or create a free Discord bot, no card needed.
1. Create the bot and invite it
Create the application and get the token
At discord.com/developers/applications, “New Application”; in the “Bot” tab, “Reset Token” and copy the token. It's the bot's password: don't share it or push it to GitHub. If it's your first bot, the slow walkthrough is in your first bot in Python or in JavaScript. Leave the “Privileged Gateway Intents” off.
Invite it with two permissions
In “OAuth2” → “URL Generator”, tick “bot” and “applications.commands”, and in the permissions “View Channels” and “Send Messages” (in total, the number 3072). “Mention Everyone” isn't needed: the bot only mentions people, never @everyone or a role. The bot permissions calculator explains each one and gives you the link.
2. The bot in Python (discord.py)
Three files in a folder. The token goes in .env, without quotes. You need Python 3.10 or newer. tzdata brings the time zones in case the system the bot runs on doesn't have them. To greet at another time, change HOUR; for another country, ZONE (for example, America/New_York or Europe/Madrid).
discord.py
python-dotenv
tzdataDISCORD_TOKEN=paste-your-token-hereimport json
import os
from datetime import date, datetime
from pathlib import Path
from zoneinfo import ZoneInfo
import discord
from discord import app_commands
from discord.ext import tasks
from dotenv import load_dotenv
load_dotenv() # reads the token from the .env file
# Birthdays are saved next to the bot: a restart doesn't lose them.
DATA = Path("birthdays.json")
ZONE = ZoneInfo("Europe/London") # change it if your group lives in another time zone
HOUR = 9 # greetings go out from this hour on
MONTHS = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"]
NO_PINGS = discord.AllowedMentions.none()
def load() -> dict:
return json.loads(DATA.read_text("utf-8")) if DATA.exists() else {}
def save(data: dict) -> None:
DATA.write_text(json.dumps(data, indent=2), "utf-8")
def server(data: dict, guild_id: int) -> dict:
return data.setdefault(str(guild_id), {"channel": None, "last": None, "birthdays": {}})
def next_birthday(day: int, month: int, today: date) -> date:
"""The next birthday from today on (today included)."""
for year in (today.year, today.year + 1):
try:
when = date(year, month, day)
except ValueError:
when = date(year, 2, 28) # born on 29 February and this isn't a leap year
if when >= today:
return when
def join(names: list[str]) -> str:
return names[0] if len(names) == 1 else ", ".join(names[:-1]) + " and " + names[-1]
class Bot(discord.Client):
def __init__(self):
# No privileged intents: each person is looked up by their ID when it's their day.
super().__init__(intents=discord.Intents.default())
self.tree = app_commands.CommandTree(self)
async def setup_hook(self):
await self.tree.sync() # publishes the commands
greet.start()
bot = Bot()
@tasks.loop(minutes=30) # checks every half hour: if the bot was off at 9, it greets when it comes back
async def greet():
now = datetime.now(ZONE)
if now.hour < HOUR:
return
today = now.date()
for guild_id, data in load().items():
if data["last"] == today.isoformat() or data["channel"] is None:
continue # already greeted today, or no channel
guild = bot.get_guild(int(guild_id))
channel = bot.get_channel(data["channel"])
if guild is None or channel is None:
continue
people = []
for user_id, (day, month) in data["birthdays"].items():
if next_birthday(day, month, today) != today:
continue
try:
await guild.fetch_member(int(user_id)) # still on the server?
except discord.HTTPException:
continue # they've left (or Discord isn't answering): they're not named
people.append(f"<@{user_id}>")
if people:
try:
await channel.send(f"🎂 Happy birthday, {join(people)}! 🎉",
allowed_mentions=discord.AllowedMentions(users=True, roles=False, everyone=False))
except discord.HTTPException as error:
print(f"Couldn't greet in {guild_id}: {error!r}")
continue # tried again on the next round
fresh = load() # read again in case someone used a command meanwhile
server(fresh, int(guild_id))["last"] = today.isoformat()
save(fresh)
@greet.before_loop
async def before_greet():
await bot.wait_until_ready()
@bot.tree.command(name="birthday", description="Save your birthday (without the year)")
@app_commands.guild_only()
@app_commands.describe(day="Day of the month", month="Month, from 1 to 12")
async def birthday(interaction: discord.Interaction, day: app_commands.Range[int, 1, 31], month: app_commands.Range[int, 1, 12]):
try:
date(2000, month, day) # 2000 was a leap year: 29 February is valid
except ValueError:
await interaction.response.send_message("That date doesn't exist.", ephemeral=True)
return
data = load()
s = server(data, interaction.guild_id)
s["birthdays"][str(interaction.user.id)] = [day, month]
save(data)
missing = "" if s["channel"] else " There's no greetings channel yet: someone who manages the server has to use /birthday-channel."
await interaction.response.send_message(f"Saved: {MONTHS[month - 1]} {day}.{missing}", ephemeral=True)
@bot.tree.command(name="forget-birthday", description="Remove your birthday from this server")
@app_commands.guild_only()
async def forget_birthday(interaction: discord.Interaction):
data = load()
removed = server(data, interaction.guild_id)["birthdays"].pop(str(interaction.user.id), None)
save(data)
text = "Removed: I won't greet you any more." if removed else "You didn't have a birthday saved."
await interaction.response.send_message(text, ephemeral=True)
@bot.tree.command(name="birthdays", description="The server's upcoming birthdays")
@app_commands.guild_only()
async def birthdays(interaction: discord.Interaction):
today = datetime.now(ZONE).date()
saved = server(load(), interaction.guild_id)["birthdays"]
if not saved:
await interaction.response.send_message("Nobody has saved their birthday yet. Use /birthday.", ephemeral=True)
return
order = sorted(saved.items(), key=lambda p: next_birthday(p[1][0], p[1][1], today))[:10]
lines = [f"{MONTHS[month - 1]} {day}: <@{user_id}>" for user_id, (day, month) in order]
# The names show, but nobody gets pinged.
await interaction.response.send_message("🎂 Upcoming birthdays:\n" + "\n".join(lines), allowed_mentions=NO_PINGS)
@bot.tree.command(name="birthday-channel", description="Choose the channel where greetings go")
@app_commands.guild_only()
@app_commands.default_permissions(manage_guild=True) # by default, only people who manage the server
async def birthday_channel(interaction: discord.Interaction, channel: discord.TextChannel):
if not channel.permissions_for(interaction.guild.me).send_messages:
await interaction.response.send_message(f"I can't write in {channel.mention}: give me “View Channel” and “Send Messages” there.", ephemeral=True)
return
data = load()
server(data, interaction.guild_id)["channel"] = channel.id
save(data)
await interaction.response.send_message(f"Done: I'll greet in {channel.mention} from {HOUR}:00.", ephemeral=True)
@bot.event
async def on_ready():
print(f"Logged in as {bot.user}")
bot.run(os.environ["DISCORD_TOKEN"])The same bot in JavaScript (discord.js)
package.json, .env and index.js. You need Node.js 20 or newer. The date and hour in the chosen time zone come from Intl.DateTimeFormat, which ships with Node: no date library needed.
{
"name": "birthday-bot",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"discord.js": "^14.16.0",
"dotenv": "^16.4.0"
}
}DISCORD_TOKEN=paste-your-token-hererequire("dotenv").config(); // reads the token from the .env file
const fs = require("node:fs");
const path = require("node:path");
const { ChannelType, Client, Events, GatewayIntentBits, InteractionContextType, MessageFlags, PermissionFlagsBits, SlashCommandBuilder } = require("discord.js");
// Birthdays are saved next to the bot: a restart doesn't lose them.
const DATA = path.join(__dirname, "birthdays.json");
const ZONE = "Europe/London"; // change it if your group lives in another time zone
const HOUR = 9; // greetings go out from this hour on
const HALF_HOUR = 30 * 60 * 1000; // checks every half hour: if the bot was off at 9, it greets when it comes back
const MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
const load = () => (fs.existsSync(DATA) ? JSON.parse(fs.readFileSync(DATA, "utf8")) : {});
const save = (data) => fs.writeFileSync(DATA, JSON.stringify(data, null, 2));
const server = (data, guildId) => (data[guildId] ??= { channel: null, last: null, birthdays: {} });
const two = (n) => String(n).padStart(2, "0");
const join = (names) => (names.length === 1 ? names[0] : `${names.slice(0, -1).join(", ")} and ${names.at(-1)}`);
// The date ("YYYY-MM-DD") and the hour right now in the chosen time zone.
function now() {
const parts = new Intl.DateTimeFormat("en-CA", { timeZone: ZONE, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", hourCycle: "h23" }).formatToParts(new Date());
const p = Object.fromEntries(parts.map((x) => [x.type, x.value]));
return { today: `${p.year}-${p.month}-${p.day}`, hour: Number(p.hour) };
}
// The next birthday from today on (today included), as "YYYY-MM-DD".
function nextBirthday(day, month, today) {
const year = Number(today.slice(0, 4));
for (const y of [year, year + 1]) {
const leap = new Date(Date.UTC(y, 1, 29)).getUTCDate() === 29;
const d = day === 29 && month === 2 && !leap ? 28 : day; // born on 29 February and this isn't a leap year
const when = `${y}-${two(month)}-${two(d)}`;
if (when >= today) return when;
}
}
// No privileged intents: each person is looked up by their ID when it's their day.
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
async function greet() {
const { today, hour } = now();
if (hour < HOUR) return;
for (const [guildId, data] of Object.entries(load())) {
if (data.last === today || !data.channel) continue; // already greeted today, or no channel
const guild = client.guilds.cache.get(guildId);
const channel = client.channels.cache.get(data.channel);
if (!guild || !channel) continue;
const people = [];
for (const [userId, [day, month]] of Object.entries(data.birthdays)) {
if (nextBirthday(day, month, today) !== today) continue;
const member = await guild.members.fetch(userId).catch(() => null); // still on the server?
if (member) people.push(`<@${userId}>`);
}
if (people.length) {
try {
await channel.send({ content: `🎂 Happy birthday, ${join(people)}! 🎉`, allowedMentions: { parse: ["users"] } });
} catch (error) {
console.error(`Couldn't greet in ${guildId}:`, error);
continue; // tried again on the next round
}
}
const fresh = load(); // read again in case someone used a command meanwhile
server(fresh, guildId).last = today;
save(fresh);
}
}
const commands = [
new SlashCommandBuilder().setName("birthday").setDescription("Save your birthday (without the year)").setContexts(InteractionContextType.Guild)
.addIntegerOption((o) => o.setName("day").setDescription("Day of the month").setRequired(true).setMinValue(1).setMaxValue(31))
.addIntegerOption((o) => o.setName("month").setDescription("Month, from 1 to 12").setRequired(true).setMinValue(1).setMaxValue(12)),
new SlashCommandBuilder().setName("forget-birthday").setDescription("Remove your birthday from this server").setContexts(InteractionContextType.Guild),
new SlashCommandBuilder().setName("birthdays").setDescription("The server's upcoming birthdays").setContexts(InteractionContextType.Guild),
new SlashCommandBuilder().setName("birthday-channel").setDescription("Choose the channel where greetings go").setContexts(InteractionContextType.Guild)
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild) // by default, only people who manage the server
.addChannelOption((o) => o.setName("channel").setDescription("Text channel").setRequired(true).addChannelTypes(ChannelType.GuildText)),
];
client.on(Events.InteractionCreate, async (i) => {
if (!i.isChatInputCommand()) return;
const hidden = { flags: MessageFlags.Ephemeral };
const data = load();
const s = server(data, i.guildId);
if (i.commandName === "birthday") {
const day = i.options.getInteger("day");
const month = i.options.getInteger("month");
if (new Date(Date.UTC(2000, month - 1, day)).getUTCDate() !== day) { // 2000 was a leap year: 29 February is valid
await i.reply({ content: "That date doesn't exist.", ...hidden });
return;
}
s.birthdays[i.user.id] = [day, month];
save(data);
const missing = s.channel ? "" : " There's no greetings channel yet: someone who manages the server has to use /birthday-channel.";
await i.reply({ content: `Saved: ${MONTHS[month - 1]} ${day}.${missing}`, ...hidden });
} else if (i.commandName === "forget-birthday") {
const had = i.user.id in s.birthdays;
delete s.birthdays[i.user.id];
save(data);
await i.reply({ content: had ? "Removed: I won't greet you any more." : "You didn't have a birthday saved.", ...hidden });
} else if (i.commandName === "birthdays") {
const { today } = now();
const order = Object.entries(s.birthdays).sort((a, b) => nextBirthday(...a[1], today).localeCompare(nextBirthday(...b[1], today))).slice(0, 10);
if (!order.length) {
await i.reply({ content: "Nobody has saved their birthday yet. Use /birthday.", ...hidden });
return;
}
const lines = order.map(([userId, [day, month]]) => `${MONTHS[month - 1]} ${day}: <@${userId}>`);
// The names show, but nobody gets pinged.
await i.reply({ content: `🎂 Upcoming birthdays:\n${lines.join("\n")}`, allowedMentions: { parse: [] } });
} else if (i.commandName === "birthday-channel") {
const channel = i.options.getChannel("channel");
if (!channel.permissionsFor(i.guild.members.me).has(PermissionFlagsBits.SendMessages)) {
await i.reply({ content: `I can't write in ${channel}: give me “View Channel” and “Send Messages” there.`, ...hidden });
return;
}
s.channel = channel.id;
save(data);
await i.reply({ content: `Done: I'll greet in ${channel} from ${HOUR}:00.`, ...hidden });
}
});
client.once(Events.ClientReady, async (c) => {
await c.application.commands.set(commands.map((x) => x.toJSON())); // publishes the commands
console.log(`Logged in as ${c.user.tag}`);
setInterval(() => greet().catch(console.error), HALF_HOUR);
greet().catch(console.error);
});
client.login(process.env.DISCORD_TOKEN);3. Try it
To test again on the same day, stop the bot, delete that server's "last" line in birthdays.json and start it. Who sees /birthday-channel can be changed without touching the code in “Server Settings” → “Integrations” → your bot.
Start the bot
In a terminal, in the folder:
pip install -r requirements.txtandpython app.py(ornpm installandnode index.js). When it says “Logged in as…”, you're set. The commands can take a few minutes to show up the first time.Choose the channel
Type
/birthday-channeland pick the text channel where you want the greetings. If the bot can't write there, it tells you right away.Save a birthday for today
Type
/birthdaywith today's day and month. If it's already past 9, the greeting shows up in the channel within half an hour at most. Then/birthdaysshows the list without pinging anyone.
The time, the time zone and 29 February
The bot doesn't use the clock of the computer it runs on, but the one in ZONE. So it doesn't matter whether it lives at your place or on a server in another country: it greets at 9 in your time zone, and handles the clock changes in March and October by itself.
It doesn't greet at 9 sharp, but on the first round from 9 on: it checks every 30 minutes and saves in birthdays.json the day it has already greeted. That's why it doesn't repeat after a restart, and doesn't skip the day if it was off at that time.
29 February only exists in leap years. In other years, someone born that day gets greeted on the 28th, and /birthdays lists them with that date.
Something not working
It doesn't greet anyone: check that someone has used /birthday-channel (when a birthday is saved with no channel, the bot says so) and that the bot can still write in that channel. If its permission was taken away, the console says “Missing Permissions” (code 50013) and it tries again on the next round.
It greets at the wrong time: change ZONE to your group's. Valid names are the ones in the time zone database, like Europe/London, America/New_York or Australia/Sydney. In Python, if it says “No time zone found”, tzdata is missing from requirements.txt.
The commands don't show up: what to check when slash commands don't appear. If the bot is online but doesn't answer, my bot doesn't respond.
Ideas to extend it
A “🎂 Birthday” role for the day: the bot also needs “Manage Roles”, its role has to sit above the one it gives, and the next day you have to remove it (save in birthdays.json who you gave it to). How to add and remove roles from code, in the role bot.
A fancier greeting, with colour and an image: swap the text for an embed (embeds in a Discord bot). And if you already have a welcome bot, you can merge both into one: on the same plan they use the same as one.
4. Keep it online all the time
A birthday bot has to be on every morning: if it lives on your computer and that's off on the day, nobody gets greeted. It has to be hosted on a server; free and paid options, compared, in where to host your Discord bot.
NapNodes has a free 256 MB bot that doesn't turn off when idle; you renew it every 7 days with “Start” (its catches, in this guide): if you forget to renew it, there are no greetings on those days. Create it, pick Node.js or Python in “Settings” → “Language”, upload your files in “Files” and click “Start”: the libraries in requirements.txt or package.json install themselves on startup, and birthdays.json stays in the bot's folder between restarts. So you don't have to renew it, the Mini plan (512 MB) stays online all the time (bot prices).
Frequently asked questions
How do I make a Discord bot wish happy birthday?
With a bot that saves each member's day and month and checks every half hour whether it's someone's day, to greet them in a channel. The full code in Python and JavaScript is in this guide.
Does the birthday bot need privileged intents?
No. Everyone saves their date with a command, and on their birthday the bot checks they're still on the server by looking them up by ID, without going through the member list.
What about people born on 29 February?
In leap years they're greeted on the 29th; in other years, on 28 February.
What permissions does the birthday bot need?
View channels and send messages (the permissions number is 3072). It doesn't need to mention everyone: it only mentions whoever has a birthday.
Where do I keep the birthday bot online all the time?
On a server. NapNodes has a free 256 MB bot that you renew with one click every week, or the Mini plan, always online.
Shall we set it up for you?
Discord bot hosting from €1.50 a month, never oversold: your RAM is yours alone. And if you'd rather start without paying, there's a free Discord bot, no card needed.
Free tool: Discord bot permissions calculator. Tick your bot's permissions and get the number and the invite link; or paste a number and see which permissions it grants.
Keep reading
- Discord Role Bot: Buttons So Everyone Picks Their Own Roles (Python and JavaScript)
Make your own Discord self-role bot in Python or JavaScript: a message with buttons to add and remove roles, no database and no reaction roles needed.
- Discord member count bot: channels with members, online and boosts (Python and JavaScript)
Build your own Discord member count bot: voice channels showing members, online and boosts that update themselves. Code in discord.py and discord.js.
- How to Make a Discord Bot in Python (discord.py), Step by Step
Make your first Discord bot in Python with discord.py: create the application, get the token, add slash commands (/hello) and keep it online for free.