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 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:

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

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.