How to Send Embeds With Your Discord Bot (discord.js and discord.py)
An embed is the card bots send: a coloured strip, a title, some text, fields in columns, an image and a footer. Here is how to send one in discord.js and in discord.py with a complete bot, how to use images from your computer, how to edit it afterwards and the limits that make Discord reject it.
What goes in an embed
Every part is optional, but an embed has to contain something: the colour of the strip on the left; the author (a name at the top, with an icon and a link); the title, which can be a link; the description, the main text; up to 25 fields, each with a name and a value, which can sit in columns (“inline”); a thumbnail at the top right; a large image at the bottom; and the footer, with an icon and the time.
A message can carry normal text and up to 10 embeds at once. The normal text goes outside the embed, above it, and that is where mentions belong: a mention inside the embed is shown, but nobody gets notified.
To see it before you code it, the embed generator draws it the way Discord does and gives you the discord.js code, the discord.py code or the JSON for a webhook.
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.
In discord.js
With EmbedBuilder: each part is a method (setTitle, setDescription, setColor, addFields, setThumbnail, setImage, setFooter, setTimestamp…) and the embed is sent in the embeds list, in channel.send or in interaction.reply. The colour accepts "#5b4ae8", a number like 0x5b4ae8 or a discord.js name such as "Blurple".
This bot publishes /info, which replies with the server card: icon and name, members, creation date (with the <t:…:D> format, which everyone sees in their own language and time zone) and the time it was sent. It needs no privileged intent. In discord.js a field is only inline if you set inline: true.
require("dotenv").config(); // reads the token from the .env file
const { Client, EmbedBuilder, Events, GatewayIntentBits, InteractionContextType, SlashCommandBuilder } = require("discord.js");
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
const info = new SlashCommandBuilder()
.setName("info")
.setDescription("Shows the server card")
.setContexts(InteractionContextType.Guild);
function tarjeta(guild) {
return new EmbedBuilder()
.setColor("#5b4ae8") // the colour of the strip on the left
.setAuthor({ name: guild.name, iconURL: guild.iconURL() ?? undefined })
.setTitle("Welcome to the server")
.setURL("https://example.com") // the title becomes a link
.setDescription("Read the **rules** before you post.")
.addFields(
{ name: "Members", value: String(guild.memberCount), inline: true },
{ name: "Created", value: `<t:${Math.floor(guild.createdTimestamp / 1000)}:D>`, inline: true },
)
.setFooter({ text: "Type /info whenever you like" })
.setTimestamp(); // the time, next to the footer
}
client.on(Events.InteractionCreate, async (i) => {
if (!i.isChatInputCommand() || i.commandName !== "info") return;
await i.reply({ embeds: [tarjeta(i.guild)] });
});
client.once(Events.ClientReady, async (c) => {
await c.application.commands.set([info.toJSON()]); // publishes /info
console.log(`Logged in as ${c.user.tag}`);
});
client.login(process.env.DISCORD_TOKEN);In discord.py
With discord.Embed(title=…, description=…, color=…, url=…, timestamp=…) and then add_field, set_author, set_thumbnail, set_image and set_footer. It is sent with embed= (one) or embeds=[…] (several), in channel.send or in interaction.response.send_message. The colour is a number (0x5B4AE8) or a discord.Color.
Two things that catch people out: in discord.py fields are inline unless you set inline=False (the opposite of discord.js), and the time must carry a time zone: discord.utils.utcnow(), not a bare datetime.now(). The same /info bot:
import os
import discord
from discord import app_commands
from dotenv import load_dotenv
load_dotenv() # reads the token from the .env file
client = discord.Client(intents=discord.Intents.default())
tree = app_commands.CommandTree(client)
def tarjeta(guild: discord.Guild) -> discord.Embed:
embed = discord.Embed(
title="Welcome to the server",
url="https://example.com", # the title becomes a link
description="Read the **rules** before you post.",
color=0x5B4AE8, # the colour of the strip on the left
timestamp=discord.utils.utcnow(), # the time, next to the footer
)
embed.set_author(name=guild.name, icon_url=guild.icon.url if guild.icon else None)
# in discord.py fields are inline unless you say inline=False
embed.add_field(name="Members", value=str(guild.member_count))
embed.add_field(name="Created", value=discord.utils.format_dt(guild.created_at, "D"))
embed.set_footer(text="Type /info whenever you like")
return embed
@tree.command(name="info", description="Shows the server card")
@app_commands.guild_only()
async def info(interaction: discord.Interaction):
await interaction.response.send_message(embed=tarjeta(interaction.guild))
@client.event
async def on_ready():
await tree.sync() # publishes /info
print(f"Logged in as {client.user}")
client.run(os.environ["DISCORD_TOKEN"])Images from your computer: attachment://
The image, the thumbnail and the author and footer icons only accept an https:// address or a file you send with the same message. To use one of your own, attach it and set the image to attachment:// plus its name. According to Discord's documentation, only .jpg, .jpeg, .png, .webp and .gif work this way.
If the image doesn't show, it is almost always because the name after attachment:// is not exactly the attached file's name (capitals included), or because the address is a web page rather than a direct image.
const { AttachmentBuilder, EmbedBuilder } = require("discord.js");
// the file name, without spaces or accents: it is what goes after attachment://
const archivo = new AttachmentBuilder("./mapa.png", { name: "mapa.png" });
const embed = new EmbedBuilder()
.setTitle("Today's map")
.setImage("attachment://mapa.png"); // the image comes from the attached file
await i.reply({ embeds: [embed], files: [archivo] });# the file name, without spaces or accents: it is what goes after attachment://
archivo = discord.File("mapa.png", filename="mapa.png")
embed = discord.Embed(title="Today's map")
embed.set_image(url="attachment://mapa.png") # the image comes from the attached file
await interaction.response.send_message(embed=embed, file=archivo)Editing an embed you already sent
A bot can edit its own messages, embeds included: handy for a scoreboard, a status that changes or marking a ticket as solved. You send the whole embed again; the easiest way is to start from the one the message already has.
// copy the embed the message already has and change only what you want
const nuevo = EmbedBuilder.from(mensaje.embeds[0]).setColor("#2ecc71").setFooter({ text: "Solved" });
await mensaje.edit({ embeds: [nuevo] });# copy the embed the message already has and change only what you want
embed = mensaje.embeds[0]
embed.color = 0x2ECC71
embed.set_footer(text="Solved")
await mensaje.edit(embed=embed)The limits, and the “Invalid Form Body” error
Discord rejects the whole message if an embed goes over one of these limits (leading and trailing spaces don't count): title, 256 characters; description, 4096; up to 25 fields; field name, 256; field value, 1024; footer, 2048; author name, 256. And across all the embeds in the message, title, description, fields, footer and author can't add up to more than 6000 characters.
When you go over, the API answers with error 50035 (“Invalid Form Body”) and says which part fails: for example embeds[0].fields[0].value in discord.js or In embeds.0.fields.0.value in discord.py. The same error appears with a field whose name or value is empty (if you compute it, put something like “—” when there is nothing) or with an address that doesn't start with http:// or https://.
If the text really is long (a list of 100 players, a full set of rules), split it: several fields, several embeds in the same message or several pages with buttons.
When it doesn't come out as expected
The message arrives without the embed: the bot lacks the “Embed Links” permission in that channel. Give it to its role or in “Edit Channel” → “Permissions”, or get a new invite link with the bot permissions calculator.
Error 50006 (“Cannot send an empty message”): the message has neither text nor an embed with something in it. It is usually an embed that ends up empty because of an earlier bug, or, in discord.js, passing the embed outside the list (embeds: embed instead of embeds: [embed]).
Bold, italics and links with text ([text](https://…)) work in the description and in field values. The title doesn't support them: to make it a link, give it the address with setURL or url=.
If you use Discord's new components (Components V2, the IS_COMPONENTS_V2 flag), that message can no longer carry embeds: the content goes in the components themselves.
Keeping it online all the time
Only a running bot sends embeds: if it lives on your computer, it switches off when you do. Free and paid options, compared, in where to host your Discord bot.
NapNodes has a free 256 MB bot that isn't switched off for being empty; you renew it every 7 days with “Start” (its downsides, in this guide). Create it, choose Node.js or Python in “Settings” → “Language” and upload your files in “Files” (or set your repository in “Settings” → “GitHub repository”); whatever the bot prints shows up in “Console”.
Frequently asked questions
How do you make an embed in discord.js?
With new EmbedBuilder() and its methods (setTitle, setDescription, setColor, addFields…), sent with channel.send({ embeds: [embed] }) or interaction.reply({ embeds: [embed] }).
How do you make an embed in discord.py?
With discord.Embed(title=…, description=…, color=…), add_field and set_footer, sent with await channel.send(embed=embed) or interaction.response.send_message(embed=embed).
Why doesn't my bot's embed show up?
It almost always lacks the “Embed Links” permission in that channel. If it's the image that's missing, check that the address is https:// and a direct image, or that the name after attachment:// matches the attached file.
How many characters fit in an embed?
256 in the title, 4096 in the description, 25 fields of 256 (name) and 1024 (value), 2048 in the footer, and across all the embeds in a message, 6000 in total.
Can you mention someone inside an embed?
The mention is shown, but they don't get notified. For the notification to arrive, put the mention in the message's normal text, outside the embed.
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 embed generator. Build an embed with a title, colour, fields and images, see it as Discord shows it and copy the discord.js, discord.py or webhook JSON code.
Keep reading
- Buttons and select menus in your Discord bot (discord.js and discord.py)
Add buttons and select menus to your Discord bot with discord.js and discord.py: pages, replying in time, keeping them after a restart and their limits.
- Slash Commands in Your Discord Bot: Options, Subcommands and Permissions
Slash commands in discord.js and discord.py: options, subcommands, permissions, slow replies, autocomplete and why they don't show up.
- Discord Logging Bot: Deleted and Edited Messages, Joins and Leaves
Make a Discord logging bot in Python or JavaScript: it records deleted and edited messages and who joins and leaves, in a channel just for moderators.