Build a Discord Bot That Removes Image Backgrounds (discord.js, 80 Lines)
Servers full of streamers, gamers and small shops keep asking the same thing in chat: "can someone remove the background from this?" A bot answers in five seconds. This is a complete discord.js bot with one slash command, /cutout, that accepts an image attachment, runs it through the bgclear API and replies with a transparent PNG. It uses free preview-size results by default, so the bot costs nothing to run until a user asks for HD.
In this guide
Setup
Create an application and bot in the Discord Developer Portal, copy the bot token and application ID, invite the bot with the applications.commands scope, and get a bgclear key from the API dashboard (10 free full-resolution credits; previews are free without limit).
npm init -y && npm i discord.js
export DISCORD_TOKEN=... DISCORD_APP_ID=... BGCLEAR_API_KEY=bgc_live_...Register the slash command
// register.js — run once (and again when the command definition changes)
import { REST, Routes, SlashCommandBuilder } from "discord.js";
const command = new SlashCommandBuilder()
.setName("cutout")
.setDescription("Remove the background from an image")
.addAttachmentOption((o) => o.setName("image").setDescription("JPG, PNG or WebP").setRequired(true))
.addBooleanOption((o) => o.setName("hd").setDescription("Full resolution (uses 1 credit)"))
.addStringOption((o) => o.setName("bg").setDescription("Background: transparent (default) or a hex colour like ffffff"));
const rest = new REST().setToken(process.env.DISCORD_TOKEN);
await rest.put(Routes.applicationCommands(process.env.DISCORD_APP_ID), { body: [command.toJSON()] });
console.log("registered /cutout");The bot
Discord gives you three seconds to acknowledge an interaction, and background removal takes longer than that, so the handler defers first and edits the reply when the PNG is ready. Attachments arrive as CDN URLs, which the API can fetch directly — no download step in the bot.
// bot.js
import { Client, GatewayIntentBits, AttachmentBuilder } from "discord.js";
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
const cooldown = new Map(); // userId -> timestamp of last call
const COOLDOWN_MS = 15_000;
client.on("interactionCreate", async (interaction) => {
if (!interaction.isChatInputCommand() || interaction.commandName !== "cutout") return;
const last = cooldown.get(interaction.user.id) ?? 0;
if (Date.now() - last < COOLDOWN_MS) {
return interaction.reply({ content: "Easy — one cutout every 15 seconds.", ephemeral: true });
}
cooldown.set(interaction.user.id, Date.now());
const attachment = interaction.options.getAttachment("image", true);
if (!attachment.contentType?.startsWith("image/")) {
return interaction.reply({ content: "Please attach a JPG, PNG or WebP.", ephemeral: true });
}
await interaction.deferReply(); // we have 3 s to ack; processing takes ~5 s
const hd = interaction.options.getBoolean("hd") ?? false;
const bg = interaction.options.getString("bg") ?? "transparent";
const res = await fetch("https://www.bgclear.ai/api/v1/remove", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.BGCLEAR_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": `discord-${attachment.id}-${hd ? "full" : "preview"}`,
},
body: JSON.stringify({ image_url: attachment.url, size: hd ? "full" : "preview", format: "png", bg_color: bg }),
});
if (!res.ok) {
const { error } = await res.json().catch(() => ({ error: { message: res.statusText } }));
const msg = res.status === 402 ? "The bot is out of HD credits — try without hd for a free preview."
: `Could not process that image (${error?.code ?? res.status}): ${error?.message ?? ""}`;
return interaction.editReply(msg);
}
const png = Buffer.from(await res.arrayBuffer());
const remaining = res.headers.get("X-Credits-Remaining");
await interaction.editReply({
content: hd ? `HD cutout · credits left: ${remaining}` : "Preview cutout (≤800 px, free). Add `hd:true` for full resolution.",
files: [new AttachmentBuilder(png, { name: "cutout.png" })],
});
});
client.login(process.env.DISCORD_TOKEN);Run node register.js once, then node bot.js. Discord's own upload limit applies to the reply (it depends on the server's boost tier); a full-resolution PNG of a very large photo can exceed it — request format: "webp" for HD replies if that happens.
Keeping it free (and when to charge)
Default to preview: it is free at the API, unlimited, unwatermarked, and 800 px is plenty for a Discord embed. Gate hd:true behind a role check (interaction.member.roles.cache.has(PREMIUM_ROLE_ID)) or a per-server allowance, because each HD image costs one credit — $9 for 100, $39 for 500, $129 for 2,000, no subscription (pricing). The idempotency key on the attachment id means a retried interaction never charges twice, and the API's 60-requests-per-minute limit is far above what a single server produces; if you run the bot in many servers, add a global queue.
For a Telegram version of the same bot, and for the hosted-proxy pattern the bot could call instead of holding the key itself, see the blog; the request format is remove.bg's, which matters if you are rewriting a bot that used remove.bg before its API closes on 1 December 2026 (mapping).
Frequently asked questions
Why deferReply?
Discord requires an acknowledgement within three seconds; background removal takes about five. deferReply shows 'thinking…' and editReply delivers the result.
Can the bot process an image someone already posted, without re-uploading?
Yes — add a message context-menu command (ApplicationCommandType.Message) and read the first attachment of the target message; the rest of the handler is identical.
Where should the API key live?
In the bot's environment on the host that runs it (a VPS, Railway, Fly.io), never in the repository. Rotate it from the dashboard if it leaks.
What does the bot cost to run?
Nothing for previews. HD cutouts are one credit each ($0.065–0.09 depending on pack); credits never expire.