DEV Community

Cover image for Moving Off the remove.bg API Before Dec 1: The Code Change in cURL, Python and Node
Oleg Tagobitsky for API4AI

Posted on Edited on

Moving Off the remove.bg API Before Dec 1: The Code Change in cURL, Python and Node

remove.bg is closing its standalone site on December 1, 2026, at 9:00 CET. Its API docs say background removal moves to Leonardo.Ai (also part of Canva) from that day, and its pricing page says unused credits expire at the same moment. If a script, a shop plugin or a Zapier step of yours calls api.remove.bg/v1.0/removebg, it needs a new home in the next six weeks.

This post is the practical part: what the options cost, what changes in the call, and the code for one of them, ours (we make the API4AI Background Removal API), next to the remove.bg call you have now.

Three ways out

  • Leonardo.Ai is remove.bg's official successor. It's a new API rather than the same one at a new address: JSON requests with a Bearer key, the image as a URL or base64 instead of a multipart upload, and the result as a link or base64 in JSON. Its migration guide lists what isn't supported, including format=zip and raw image bytes in the response. Background removal costs $0.1047 an image.
  • Photoroom has built its API to be a drop-in: per its own migration post, you change the endpoint URL and the key, and the main parameters (image_file, crop, bg_color, format, size) carry over. $0.02 an image on its Basic plan.
  • A different API with a thin adapter. That's the rest of this post. The request changes a little, the response changes more, and an 80-line Python function hides both from the rest of your code.

If you want zero code changes, Photoroom is the honest answer. If you can spend half an hour on an adapter, read on.

What it costs per month

List prices, read on each company's public page on Oct 21, 2026. For remove.bg, the smallest subscription that covers the volume (it stops on Dec 1, so this is what you pay now, not an option).

Images a month remove.bg (subscription) Leonardo.Ai Photoroom (Basic) API4AI
1,000 $189 (1,200-credit plan) $104.70 $20 $5
10,000 $989 $1,047 $200 $50
50,000 $3,950 $5,235 $1,000 $250

Photoroom offers volume discounts on its Enterprise plan from 200,000 images a year, and we give a volume price from about 15,000 images a month (write to hello@api4.ai). We're not the cheapest background removal API on the market, and we don't claim to be. What we offer: pay as you go with no subscription, a balance that never expires, no GPU to run, commercial use of the results (see our Terms of Use), and $5 of free credit when you sign up, enough for 1,000 images.

What changes in the call

remove.bg API4AI
Endpoint POST https://api.remove.bg/v1.0/removebg POST https://api4ai.cloud/img-bg-removal/v1/results
Key X-Api-Key header X-API-KEY header (header names aren't case-sensitive, so the same line works with the new key)
A file multipart field image_file multipart field image
A link image_url url
Result image bytes in the body JSON, with the PNG as base64 in results[0].entities[]
A photo it can't process an HTTP error status HTTP 200 with status.code = "failure" and a reason
Input size up to 25 megapixels with size=auto (its docs) JPEG or PNG, under 16 MB, up to 4096×4096
size, format, crop, bg_color parameters the result is always a full-size PNG with a transparent background; crop, color and JPG/WebP are a few lines of Pillow (in the adapter below)
Your own background image bg_image_file, bg_image_url image-bg, url-bg
Shadows, type, roi, scale, position, ZIP output supported not available

The last row matters: if you rely on remove.bg's shadows or its car and product modes, check the result on your own photos before you switch. The demo on our Background Removal page takes any JPEG or PNG without an account.

The same call, before and after

cURL

remove.bg:

curl -H "X-Api-Key: $REMOVEBG_KEY" \
     -F "image_file=@car.jpg" \
     -F "size=auto" \
     -f https://api.remove.bg/v1.0/removebg -o no-bg.png
Enter fullscreen mode Exit fullscreen mode

API4AI (the PNG is inside the JSON, so jq picks it out and base64 decodes it):

curl -s -X POST "https://api4ai.cloud/img-bg-removal/v1/results" \
     -H "X-API-KEY: $API4AI_KEY" \
     -F "image=@car.jpg" \
  | jq -r '.results[0].entities[] | select(.kind == "image") | .image' \
  | base64 --decode > no-bg.png
Enter fullscreen mode Exit fullscreen mode

Python

remove.bg:

import requests

r = requests.post(
    "https://api.remove.bg/v1.0/removebg",
    headers={"X-Api-Key": REMOVEBG_KEY},
    files={"image_file": open("car.jpg", "rb")},
    data={"size": "auto"},
)
r.raise_for_status()
open("no-bg.png", "wb").write(r.content)
Enter fullscreen mode Exit fullscreen mode

API4AI:

import base64
import requests

r = requests.post(
    "https://api4ai.cloud/img-bg-removal/v1/results",
    headers={"X-API-KEY": API4AI_KEY},
    files={"image": open("car.jpg", "rb")},
)
r.raise_for_status()
result = r.json()["results"][0]
if result["status"]["code"] != "ok":                # HTTP 200 can still be a failure
    raise RuntimeError(result["status"]["message"])
image = next(e for e in result["entities"] if e["kind"] == "image")
open("no-bg.png", "wb").write(base64.b64decode(image["image"]))
Enter fullscreen mode Exit fullscreen mode

Node (18+)

remove.bg:

import fs from "node:fs";

const form = new FormData();
form.append("size", "auto");
form.append("image_file", await fs.openAsBlob("car.jpg"), "car.jpg");

const res = await fetch("https://api.remove.bg/v1.0/removebg", {
  method: "POST",
  headers: { "X-Api-Key": process.env.REMOVEBG_KEY },
  body: form,
});
if (!res.ok) throw new Error(`${res.status}: ${res.statusText}`);
fs.writeFileSync("no-bg.png", Buffer.from(await res.arrayBuffer()));
Enter fullscreen mode Exit fullscreen mode

API4AI:

import fs from "node:fs";

const form = new FormData();
form.append("image", await fs.openAsBlob("car.jpg"), "car.jpg");

const res = await fetch("https://api4ai.cloud/img-bg-removal/v1/results", {
  method: "POST",
  headers: { "X-API-KEY": process.env.API4AI_KEY },
  body: form,
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);

const result = (await res.json()).results[0];
if (result.status.code !== "ok") throw new Error(result.status.message);
const image = result.entities.find((e) => e.kind === "image");
fs.writeFileSync("no-bg.png", Buffer.from(image.image, "base64"));
Enter fullscreen mode Exit fullscreen mode

The one line not to skip

When our service can't process a photo (it's corrupted, isn't really a JPEG or PNG, or is bigger than 4096×4096), it still answers HTTP 200. The JSON says "failure" and gives the reason, for example "Resolution is too big: 6000x4000. Max allowed resolution: 4096x4096."

remove.bg code usually checks only the HTTP status, because that's how remove.bg reports errors. Port that check as it is and a failed photo looks like a success that wrote nothing. Every sample above checks status.code; keep that line when you paste them into your code.

A stand-in for remove_bg()

If remove.bg calls are spread across your code, it's easier to replace the function than every call. This one takes the main remove.bg arguments, returns image bytes like remove.bg's response body, and does crop, bg_color and JPG/WebP output locally with Pillow. Anything it can't do raises an error that names the argument, so nothing is silently dropped.

"""A stand-in for the remove.bg call, on the API4AI Background Removal API.

    pip install requests pillow
    export API4AI_KEY=...            # from portal.api4.ai

Your code keeps calling remove_bg(...) with the main remove.bg arguments and
gets image bytes back. Anything it can't do raises an error with the name of
the argument, instead of being silently ignored.
"""
import base64
import io
import os
import re
import time

import requests
from PIL import Image, ImageColor

URL = "https://api4ai.cloud/img-bg-removal/v1/results"


class RemoveBgError(Exception):
    pass


def _call(image_file, image_url, tries=4):
    headers = {"X-API-KEY": os.environ["API4AI_KEY"]}
    for attempt in range(tries):
        if image_file:
            with open(image_file, "rb") as f:
                r = requests.post(URL, headers=headers, files={"image": f}, timeout=120)
        else:   # a link goes in as a multipart field too
            r = requests.post(URL, headers=headers, files={"url": (None, image_url)}, timeout=120)
        if r.status_code in (429, 500, 502, 503, 504) and attempt < tries - 1:
            time.sleep(2 ** attempt)                          # 1, 2, 4 s
            continue
        if r.status_code != 200:
            raise RemoveBgError(f"HTTP {r.status_code}: {r.text[:200]}")
        result = r.json()["results"][0]
        # A photo the service can't process still comes back as HTTP 200.
        if result["status"]["code"] != "ok":
            raise RemoveBgError(result["status"]["message"])
        image = next(e for e in result["entities"] if e["kind"] == "image")
        return Image.open(io.BytesIO(base64.b64decode(image["image"])))


def remove_bg(image_file=None, image_url=None, size="auto", format="png",
              bg_color=None, crop=False, **other):
    """Returns the cut-out as bytes, like the body of a remove.bg response."""
    if other:
        raise RemoveBgError("not supported: " + ", ".join(sorted(other)))
    if size not in ("auto", "full"):
        raise RemoveBgError(f"size={size!r}: results come back at full size")
    if bool(image_file) == bool(image_url):
        raise RemoveBgError("pass image_file or image_url")
    if format.lower() not in ("png", "jpg", "webp"):
        raise RemoveBgError(f"format={format!r}: use png, jpg or webp")

    png = _call(image_file, image_url)            # RGBA, background transparent
    if crop:
        png = png.crop(png.getchannel("A").getbbox())
    if bg_color or format.lower() == "jpg":       # JPG has no transparency
        color = bg_color or "ffffff"
        if re.fullmatch(r"[0-9a-fA-F]{6}", color):
            color = "#" + color
        flat = Image.new("RGBA", png.size, ImageColor.getrgb(color))
        png = Image.alpha_composite(flat, png)

    out = io.BytesIO()
    if format.lower() == "jpg":
        png.convert("RGB").save(out, "JPEG", quality=92)
    else:
        png.save(out, format.upper())
    return out.getvalue()


if __name__ == "__main__":
    import sys
    with open("no-bg.png", "wb") as f:
        f.write(remove_bg(image_file=sys.argv[1]))
Enter fullscreen mode Exit fullscreen mode

Use it the way you used remove.bg:

from removebg_adapter import remove_bg, RemoveBgError

png = remove_bg(image_file="car.jpg", size="auto")
jpg = remove_bg(image_url="https://example.com/shoe.jpg", bg_color="ffffff", crop=True, format="jpg")
Enter fullscreen mode Exit fullscreen mode

Three things it decides for you, which you may want to change:

  • size accepts auto and full only. Our results come back at the size of the input, so the smaller, cheaper remove.bg sizes have no match; the function says so rather than returning a full-size image you didn't ask for.
  • JPG without bg_color gets a white background, because JPG can't be transparent.
  • Retries: a 429 or a 5xx waits 1, 2, then 4 seconds. If you call it from many threads at once, keep an eye on 429s.

No-code: Zapier, Make and n8n

If a no-code flow has a remove.bg step, you don't need a new app or template to replace it. Swap that step for the tool's plain HTTP request (Webhooks by Zapier, Make's HTTP module, n8n's HTTP Request node): POST to https://api4ai.cloud/img-bg-removal/v1/results, an X-API-KEY header with your key, and a multipart form field url with a public link to the photo (or image with the file). The cut-out comes back as base64 in results[0].entities[0].image. In n8n, the Convert to File node turns it into a file; in Make, the toBinary() function does; in Zapier you'll need a short Code step.

Before Dec 1

  1. Put five of your hardest photos through the demo: glass, hair, white on white.
  2. Get a key on the API4AI developer portal. No credit card, $5 of free credit, pay as you go after that at $5 per 1,000 images, and the balance never expires.
  3. Swap the call or drop in the adapter, and run a day's photos through both before your remove.bg credits run out.

The full reference is in the docs, and the price table with the sources is on our moving from remove.bg page.

Which remove.bg parameter would you miss most if it went away?

Top comments (0)