How to Send a Base64 Image with Discord.js
You have a Base64 string — from an API response, a database column, or a converter — and you pass it straight into channel.send({ files: [...] }) or drop it into embed.setImage(). What comes back is Invalid Form Body, an ENOENT throw, or an embed that simply renders blank. That is not a bug in your code. Discord.js does not accept Base64 text as a file, and Discord embeds do not accept data: URIs as image URLs. The fix is always the same: decode the string to a Buffer, wrap it in an attachment, and (for embeds) reference it via attachment://.
The short answer
The files option of channel.send(), interaction.reply() and webhook executes accepts exactly these shapes:
- A
BufferorUint8Array— the decoded bytes, wrapped in anAttachmentBuilderwith a filename. - A readable stream — for example
fs.createReadStream(). - A file path — discord.js reads it from disk for you.
- A public http(s) URL — the library fetches it.
A Base64 string is none of the four. And EmbedBuilder.setImage() is stricter still: it accepts an attachment://name reference or a public http(s) URL — never a data: URI. The fix: strip the prefix, Buffer.from(str, 'base64'), attach, reference.
Why a Base64 string fails
Discord's REST API receives files as multipart/form-data: binary parts with names. Base64 is a text encoding that exists to smuggle binary through text-only channels such as JSON — which is why AI vision APIs do accept data URIs (see the API payload guide), while file-upload endpoints like Discord's are the opposite rule.
The failure modes follow directly:
- Plain string passed as a file → discord.js interprets it as a path, tries to open a file with a 40,000-character name, and throws ENOENT.
- Base64 inside JSON body → the REST endpoint wants multipart, not JSON, so the API answers Invalid Form Body.
data:URI insetImage()→ Discord's embed proxy only fetches http(s) URLs, so the field fails validation and the embed ships with a blank image slot.
The fix: decode, wrap, send
The decode step is identical everywhere: cut everything up to the first comma, then Base64-decode the rest into a Buffer.
Discord.js v14 — AttachmentBuilder
import { AttachmentBuilder } from 'discord.js';
const base64Only = dataUri.includes(',') ? dataUri.split(',')[1] : dataUri;
const buffer = Buffer.from(base64Only, 'base64');
const attachment = new AttachmentBuilder(buffer, { name: 'image.png' });
await channel.send({ files: [attachment] });
Discord.js v13 — MessageAttachment
const buffer = Buffer.from(base64Only, 'base64');
const attachment = new MessageAttachment(buffer, 'image.png');
await channel.send({ files: [attachment] });
MessageAttachment still exists in v14 as a deprecated alias — same class, new name. Patch old code by renaming the import; nothing else changes.
discord.py (Python)
import base64, io, discord
raw = base64.b64decode(data_uri.split(",", 1)[1]) # strip prefix, decode
file = discord.File(io.BytesIO(raw), filename="image.png")
await channel.send(file=file)
Showing it inside an embed
Embeds cannot carry the bytes themselves — the image has to ride along as a file in the same message, referenced by its filename:
const { AttachmentBuilder, EmbedBuilder } = require('discord.js');
const attachment = new AttachmentBuilder(buffer, { name: 'image.png' });
const embed = new EmbedBuilder()
.setTitle('Generated report')
.setImage('attachment://image.png'); // must match the file name above
await channel.send({ embeds: [embed], files: [attachment] });
The attachment:// scheme is the part most answers miss: without a file of the same name in the same message, the embed renders blank. A thumbnail works identically via .setThumbnail('attachment://image.png'). If the image already lives at a public https URL, skip the upload and pass that URL to setImage() directly — Discord fetches and re-hosts it on its CDN.
Size limits and gotchas
- 25 MiB per message by default. Server boost tiers raise the ceiling. Exceeding it returns 413 Request Entity Too Large. Remember Base64 inflates the payload by about 33% — the size calculator shows the overhead before you build the string.
- ENOENT means "you passed the string itself". It is the signature error for a missing
Buffer.from(..., 'base64')step — discord.js read your Base64 as a file path. - Keep the prefix decision straight. The
data:prefix matters only to renderers (HTML/CSS). APIs and libraries want raw payload or decoded bytes — the invalid Base64 guide covers the prefix bugs that survive into requests. - Wrapped lines are fine.
Buffer.from(str, 'base64')ignores whitespace and newlines, so 76-character line breaks do not break the decode.
Frequently asked questions
Can discord.js accept a Base64 string directly as a file?
No. The file option accepts a Buffer, a ReadStream, a path or a public URL. Decode the string with Buffer.from(str, 'base64') and wrap it in an AttachmentBuilder.
Why is my embed image blank?
A data: URI is not a fetchable embed URL. Upload the image as a file in the same message and use setImage('attachment://image.png'), or pass a public https URL.
Why do I get ENOENT?
The Base64 string was passed as the file itself, so discord.js treated it as a path. Decode it to a Buffer first.
How large can the upload be?
25 MiB per message by default, more with boost tiers — 413 when exceeded. The size calculator shows what Base64 does to the payload.
AttachmentBuilder or MessageAttachment?
Same class. v14 renamed MessageAttachment to AttachmentBuilder; the old name lingers as a deprecated alias.
Does this work for webhooks?
Yes — webhook executes take the same files array with AttachmentBuilder objects, and embeds reference them via attachment://.
Got an opaque Base64 blob back from an API and not sure what it is? The Base64 to Image decoder identifies the format from the magic bytes and previews it. Posting to other chat platforms goes through the same decode-then-upload pattern — see the Slack no_file_data fix and the Telegram sendPhoto guide — while AI vision APIs want the opposite shape, as the Image to Base64 for API page explains. The homepage converter produces plain data URIs for HTML, CSS and email.