Remove Image Backgrounds in Node.js with fetch and FormData (No SDK)
Node 18 ships fetch, FormData and Blob, which is everything a background-removal call needs — no axios, no form-data package, no SDK. This tutorial covers the direct call, a URL-based JSON call, an Express endpoint that keeps your key server-side, and the two details that make it production-safe: idempotent retries and rate-limit handling.
In this guide
Direct call: file in, PNG out
import fs from "node:fs/promises";
const API = "https://www.bgclear.ai/api/v1/remove";
const KEY = process.env.BGCLEAR_API_KEY;
export async function removeBackground(inPath, outPath, size = "full") {
const form = new FormData();
form.append("image_file", new Blob([await fs.readFile(inPath)]), inPath);
form.append("size", size); // preview (free, ≤800px) | full | auto
form.append("format", "png"); // png | webp | jpg
const res = await fetch(API, {
method: "POST",
headers: { Authorization: `Bearer ${KEY}` },
body: form,
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
await fs.writeFile(outPath, Buffer.from(await res.arrayBuffer()));
console.log("credits remaining:", res.headers.get("X-Credits-Remaining"));
}
await removeBackground("photo.jpg", "photo-no-bg.png");Do not set Content-Type yourself — fetch adds the multipart boundary. The field names are the remove.bg ones, so if you are migrating from remove.bg, only the URL and the header change.
URL input with a JSON response
When the source image is already online, send JSON and ask for JSON back. The response carries a hosted result URL (valid 24 hours, no auth needed to fetch) that you can store or hand to the next service.
const res = await fetch(API, {
method: "POST",
headers: {
Authorization: `Bearer ${KEY}`,
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
image_url: "https://example.com/product.jpg",
size: "auto",
format: "webp",
bg_color: "ffffff", // omit for transparent
}),
});
const data = await res.json();
// { id, url, width, height, credits_charged, credits_remaining, processing_ms }An Express endpoint that hides the key
Never call the API from the browser — the key would ship in your bundle. Proxy through your server: the browser uploads to you, you forward to the API, you stream the PNG back.
import express from "express";
import multer from "multer";
const app = express();
const upload = multer({ limits: { fileSize: 25 * 1024 * 1024 } });
app.post("/api/cutout", upload.single("image"), async (req, res) => {
const form = new FormData();
form.append("image_file", new Blob([req.file.buffer]), req.file.originalname);
form.append("size", "auto");
const upstream = await fetch("https://www.bgclear.ai/api/v1/remove", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.BGCLEAR_API_KEY}`,
// one key per upload → a browser retry can never charge twice
"Idempotency-Key": `${req.file.size}-${req.file.originalname}-${req.ip}`,
},
body: form,
});
if (!upstream.ok) return res.status(upstream.status).json(await upstream.json());
res.type(upstream.headers.get("content-type"));
res.send(Buffer.from(await upstream.arrayBuffer()));
});
app.listen(3000);Add your own auth and a per-user quota in front of this route; otherwise anyone who finds it can spend your credits.
Retries and rate limits
The API allows 60 requests a minute per key and answers 429 with an X-RateLimit-Reset header (seconds). Combine that with the idempotency header for a retry loop that is safe to run in a queue worker.
async function withRetry(fn, tries = 3) {
for (let i = 0; i < tries; i++) {
const res = await fn();
if (res.ok) return res;
if (res.status === 429) {
const wait = Number(res.headers.get("X-RateLimit-Reset") ?? 5);
await new Promise((r) => setTimeout(r, wait * 1000));
continue;
}
if (res.status === 402) throw new Error("Out of credits — top up at bgclear.ai/api-pricing/");
if (res.status >= 500) continue; // processing_failed / gpu_unavailable → retry
throw new Error(await res.text()); // 4xx: fix the request
}
throw new Error("gave up after retries");
}Large images: jobs and polling
Sync calls at full size are limited to 4 megapixels. Bigger images (up to 50 MP) go to /api/v1/jobs, which returns a job id immediately.
const JOBS = "https://www.bgclear.ai/api/v1/jobs";
const auth = { Authorization: `Bearer ${KEY}` };
const form = new FormData();
form.append("image_file", new Blob([await fs.readFile("huge.jpg")]), "huge.jpg");
form.append("size", "full");
const { job_id } = await (await fetch(JOBS, { method: "POST", headers: auth, body: form })).json();
let job;
do {
await new Promise((r) => setTimeout(r, 2000));
job = await (await fetch(`${JOBS}/${job_id}`, { headers: auth })).json();
} while (job.status === "queued" || job.status === "processing");
if (job.status === "done") {
const png = await (await fetch(job.url)).arrayBuffer(); // no auth needed, 24 h
await fs.writeFile("huge-no-bg.png", Buffer.from(png));
}Prefer an X-Callback-Url header over polling in production; the API POSTs to it when the job finishes. Batches of up to 50 URLs go through /api/v1/jobs/batch.
Frequently asked questions
Does this work in Bun or Deno?
Yes — both implement fetch, FormData and Blob the same way; only the file-reading calls differ.
Can I call the API from the browser directly?
Technically yes, but it exposes your key. Proxy through a server route as shown, and add your own auth and quota.
Which output format should I use?
png for transparency with maximum compatibility, webp for transparency at a much smaller size, jpg with bg_color for marketplace listings that need a solid background.
How much does it cost?
Preview results (≤800 px) are free. Full-resolution results are one credit each: $9 for 100, $39 for 500, $129 for 2,000, credits never expire.