NapNodes

← Discord bot guides

Discord bots · updated 31 October 2026

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

  1. 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.

  2. 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).

requirements.txt
discord.py
python-dotenv
tzdata
.env
DISCORD_TOKEN=paste-your-token-here
app.py
import 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.

package.json
{
  "name": "birthday-bot",
  "version": "1.0.0",
  "main": "index.js",
  "dependencies": {
    "discord.js": "^14.16.0",
    "dotenv": "^16.4.0"
  }
}
.env
DISCORD_TOKEN=paste-your-token-here
index.js
require("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.

  1. Start the bot

    In a terminal, in the folder: pip install -r requirements.txt and python app.py (or npm install and node index.js). When it says “Logged in as…”, you're set. The commands can take a few minutes to show up the first time.

  2. Choose the channel

    Type /birthday-channel and pick the text channel where you want the greetings. If the bot can't write there, it tells you right away.

  3. Save a birthday for today

    Type /birthday with 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 /birthdays shows 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

All Discord bot guides, sorted →