What the official sources actually say
remove.bg's API page carries the announcement in one sentence:
“Starting December 1, 2026, background removal moves to Leonardo.Ai, also part of Canva.”
remove.bg API documentation
Leonardo's migration guide supplies the mechanics: the endpoint, the authentication header, the JSON shape, the parameter mapping, and a list of remove.bg features that are not carried over. The guide is written for developers, and it is the only source this article follows for request and response details. If you want a local copy, appending .md to its URL returns the same page as markdown.
One thing neither page states is a time of day. The date is published; the hour is not. Any article quoting a precise cutover minute is adding something the vendors have not published, so plan by date and verify on the day.
- Migration date
- December 1, 2026. No time of day is published by either vendor.
- Authentication
Authorization: Bearer <key>, created in app.leonardo.ai under API Access.- Endpoint
POST /api/rest/v2/generationssyncon cloud.leonardo.ai
At a glance: remove.bg versus Leonardo Sync
This is the table to screenshot. It is the whole migration in thirteen rows.
| Concern | remove.bg | Leonardo Sync |
|---|---|---|
| Authentication | X-Api-Key: <key> | Authorization: Bearer <LEONARDO_API_KEY> |
| Endpoint | POST api.remove.bg/v1.0/removebg | POST cloud.leonardo.ai/api/rest/v2/generationssync |
| Content type | multipart/form-data or JSON | application/json only |
| Model | Implicit | Explicit: "model": "remove-bg" |
| Source image | image_file, image_url, image_file_b64 | One image object in parameters.guidances.image_reference: URL, BASE64, or uploaded ID |
| Options | Top-level form fields | Same names, nested under parameters |
| Response | Raw image bytes by default | Always JSON: results[0].url, or results[0].dataB64 when base64 is true |
| Storage | Nothing kept | Saved to your Leonardo library by default; ephemeral: true keeps nothing and returns a 30-minute URL |
| Output formats | png, jpg, webp, zip, auto | png, jpg, webp — no zip, no auto |
| Cost signal | X-Credits-Charged header | cost object in the body, in USD |
| Error shape | HTTP status plus body | Usually HTTP 200 with errors[]; read extensions.statusCode and extensions.code |
| Input size | 22 MB | Up to 25 MB for URL or upload; base64 under about 7.5 MB |
| Foreground metadata | X-Type, X-Foreground-* | No equivalent |
Before you start
Create the key on the Leonardo side. The key comes from the API Access page at app.leonardo.ai and is sent as a bearer token on every request. A remove.bg key will not authenticate against the new endpoint, and there is no translation layer that accepts one.
Budget per image, not per month. Leonardo's documentation states that background removal costs USD 0.1047 per image and returns the charge for each call in the response body. The API is billed pay-as-you-go from an account balance, with manual or automatic top-ups; there is no monthly commitment to sign.
Do not budget on a free allowance you have not seen. remove.bg's own API page says the first 50 API calls per month are on them. Leonardo's official documentation describes pay-as-you-go pricing and does not document a monthly free allowance for this model, so any “500 images” figure circulating in third-party write-ups is unverified. Check the pricing calculator inside your account before you commit to volume.
Decide on storage before the first request. The default persists inputs and outputs to your Leonardo library. If your product handles customer images, set ephemeral: true and read the URL promptly, because it expires after 30 minutes.
The request, decoded
Every call has the same skeleton. The model name is a literal, the options move under parameters, and exactly one image object goes into guidances.image_reference:
{
"model": "remove-bg",
"ephemeral": true,
"parameters": {
"format": "png",
"size": "auto",
"guidances": {
"image_reference": [
{ "image": { "type": "URL", "url": "https://example.com/logo.png" } }
]
}
}
}The image object takes one source and one source only: {"type": "URL", "url": …}, {"type": "BASE64", "data": …}, or {"type": "UPLOADED", "id": …} for an image already in the library. Leonardo's model guide also notes that unknown keys under parameters are rejected rather than ignored, which is useful: a typo fails loudly instead of silently falling back to a default. Set format explicitly, because auto is not accepted here.
Python: file in, transparent PNG out
Standard library only, so there is nothing to install. It reads a local file, base64-encodes it, posts the JSON body, and writes the returned PNG. The token comes from the environment.
import base64
import json
import os
import pathlib
import urllib.request
API_KEY = os.environ["LEONARDO_API_KEY"]
API_BASE = os.environ.get("LEONARDO_API_BASE", "https://cloud.leonardo.ai")
SOURCE = pathlib.Path("logo.png")
image_b64 = base64.b64encode(SOURCE.read_bytes()).decode("ascii")
body = {
"model": "remove-bg",
"ephemeral": True,
"base64": True,
"parameters": {
"size": "auto",
"format": "png",
"guidances": {
"image_reference": [{"image": {"type": "BASE64", "data": image_b64}}]
},
},
}
request = urllib.request.Request(
f"{API_BASE}/api/rest/v2/generationssync",
data=json.dumps(body).encode("utf-8"),
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request, timeout=120) as response:
payload = json.loads(response.read())
if payload.get("errors"):
raise SystemExit(payload["errors"][0].get("message", str(payload["errors"])))
results = payload.get("results") or []
if not results:
raise SystemExit("No result returned. Check moderation output and results[].")
target = pathlib.Path("logo-no-bg.png")
if results[0].get("dataB64"):
target.write_bytes(base64.b64decode(results[0]["dataB64"]))
else:
with urllib.request.urlopen(results[0]["url"], timeout=120) as image:
target.write_bytes(image.read())
print(f"Wrote {target} ({target.stat().st_size} bytes)")Two details are deliberate. The script checks payload["errors"] before touching results, because a Hasura-style response can report a failed call with HTTP 200. And it handles both response shapes: dataB64 when it is present, the URL otherwise. That fallback matters when an output exceeds the inline base64 ceiling.
Node.js: the same flow
Node 18 or newer, because the fetch API is built in. Same environment variable, same body, same two response branches.
import fs from "node:fs/promises";
const API_KEY = process.env.LEONARDO_API_KEY;
const API_BASE = process.env.LEONARDO_API_BASE ?? "https://cloud.leonardo.ai";
if (!API_KEY) throw new Error("Set LEONARDO_API_KEY before running this script.");
const imageB64 = (await fs.readFile("logo.png")).toString("base64");
const response = await fetch(`${API_BASE}/api/rest/v2/generationssync`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
model: "remove-bg",
ephemeral: true,
base64: true,
parameters: {
size: "auto",
format: "png",
guidances: {
image_reference: [{ image: { type: "BASE64", data: imageB64 } }],
},
},
}),
});
const payload = await response.json();
if (payload.errors?.length) {
throw new Error(payload.errors[0].message ?? JSON.stringify(payload.errors));
}
const result = payload.results?.[0];
if (!result) throw new Error("No result returned. Check moderation output and results[].");
if (result.dataB64) {
await fs.writeFile("logo-no-bg.png", Buffer.from(result.dataB64, "base64"));
} else {
const image = await fetch(result.url);
await fs.writeFile("logo-no-bg.png", Buffer.from(await image.arrayBuffer()));
}
console.log("Wrote logo-no-bg.png", payload.cost);Both scripts read LEONARDO_API_BASE only so you can point them at a proxy or a test double; in production you never set it. Neither script writes the token into the file, and neither prints it.
Reading the response
The sync call always returns JSON. A successful body carries an id, a cost object, a blockedCount, and a results array with one image:
{
"id": "b3a6c1d2-…",
"blockedCount": 0,
"cost": { "amount": "0.1047", "unit": "DOLLARS" },
"results": [
{
"url": "https://…",
"contentType": "image/png",
"width": 1024,
"height": 768
}
]
}results is an array, and the model guide warns that it can be empty when moderation withholds an output. Guard the index access. With base64: true, the same array carries dataB64 instead of url, and that inline path only works for outputs under roughly 7.45 MB of encoded pixels. For large outputs, drop base64 and fetch the URL.
The metadata that used to travel in headers is split. contentType replaces Content-Type, the width and height become fields, and cost replaces X-Credits-Charged. The detected foreground type and its bounding box do not come back at all.
Errors, rate limits, and retries
The error convention is the one most likely to break an existing client. Leonardo's migration guide states that Hasura usually returns HTTP 200 with an errors[] array, and that you should read errors[0].message along with extensions.statusCode and extensions.code. A client that only checks response.ok will treat a failed removal as a success and then crash on a missing results[0].
Keep your 429 and 5xx backoff logic. The transport changed; the need to retry with increasing delays did not. And watch the clock: the sync endpoint may hold the connection for the better part of half a minute before it times out, which is fine for a webhook-driven job queue and wrong for a request a user is waiting on. That is the boundary where the async API belongs.
Storage and privacy
This is a behaviour change, not a parameter rename. remove.bg stored nothing; Leonardo persists the input and the output to your library and CDN by default. For a migration that handled customer images, that is the difference between “we never kept it” and “we kept it until someone deletes it”.
Set "ephemeral": true to match the old behaviour: no lasting library assets, and the response returns a 30-minute presigned URL. Add "base64": true if you would rather have the bytes inline than chase that URL. Billing is the same either way, because the flag controls retention, not usage.
Not yet supported: what breaks, and what to do
This is the part most migration write-ups skip, and it is the part that decides whether your pipeline survives the cutover. Leonardo's guide is explicit that these are not temporary gaps in the documentation:
| remove.bg feature | Status on Leonardo | What to do instead |
|---|---|---|
format=zip (colour plus alpha matte) | Will not be supported | Request png or webp directly and remove the client-side composition step; use channels=alpha only if you genuinely need the matte |
format=auto | Will not be supported | Choose png, jpg, or webp in the request |
bg_image_file | No equivalent | Host the background image and pass guidances.background_image_reference as a URL |
| Raw image bytes as the response | Will not be supported | Download results[0].url, or decode dataB64 when base64 is true |
multipart/form-data uploads | Will not be supported | Base64-encode files under about 7.5 MB, or upload once and reference the returned image ID |
X-Type and foreground-position metadata | Will not be supported | Classify on your side if you depend on it, or drop the dependency |
POST /improve (image contribution) | Will not be supported | Remove the call; there is no contributed-training path |
| OAuth 2.0 end-user login | Will not be supported | Authenticate with your account's API key |
When one of these hits you, there are two honest paths. The first is to wait: if your product cannot ship without ZIP output or foreground metadata, you need a different provider or an internal wrapper, not a rename. The second is to move the work to where the constraint disappears.
For occasional cutouts, that second path is simply local processing. NoBGNow runs the model inside the browser, so there is no upload, no API key, and no retention question to answer — the file never leaves the device. It is the right shape for a designer who needs one transparent logo, and the wrong shape for a batch pipeline that needs ten thousand. If that is your case, the logo background remover is the page to open, or start from the background remover and pick the file.
The async API, briefly
The async endpoint is the same request body without ephemeral and base64, posted to cloud.leonardo.ai/api/rest/v2/generations. It returns a generationId immediately; you then retrieve the generation by ID, or configure a webhook so you do not have to poll. Results from the async path are persisted, with durable CDN URLs. If your queue already retries on failure, the move from sync to async is mostly a change of address.
Migration checklist
- Create a Leonardo API key and replace the
X-Api-Keyheader withAuthorization: Bearer. - Point requests at
POST https://cloud.leonardo.ai/api/rest/v2/generationssync. - Send
Content-Type: application/jsonwith"model": "remove-bg". - Move the source image into
parameters.guidances.image_reference: base64 under about 7.5 MB, URL or upload above it. - Move the options under
parameters; the values themselves do not change. - Replace
format=autoandformat=zipwith an explicitpng,jpg, orwebp. - Read
results[0].urlorresults[0].dataB64, and read cost fromcostinstead of the header. - Set
ephemeral: trueif your privacy promise is “nothing is kept”. - Keep the 429 and 5xx retries, and parse the Leonardo error shape.
- Audit for
bg_image_fileand ZIP composition before you switch traffic, not after.
Frequently asked questions
- When exactly does the remove.bg API stop working?
- remove.bg publishes one date: December 1, 2026. Neither remove.bg's API page nor Leonardo's migration guide names a time of day, so treat any specific hour you see quoted elsewhere as unconfirmed and plan the cutover by date.
- Is there a free tier on Leonardo.Ai for background removal?
- remove.bg's API page says the first 50 API calls per month are on them. Leonardo's official documentation describes pay-as-you-go pricing only, at USD 0.1047 per background removal, and does not document a monthly free allowance. Check the pricing calculator in your own account before you budget.
- Does Leonardo support ZIP output or format=auto?
- No. Leonardo's migration guide lists both as not supported and states they will not be supported. Request png, jpg, or webp explicitly; PNG and WebP keep transparency.
- Will my customers' images be stored?
- By default the input and output are saved to your Leonardo library. Set
ephemeralto true to keep nothing: the response then returns a 30-minute URL instead of a durable one. Billing is the same either way. - Can I avoid moving to an API at all?
- Yes, if you only need occasional cutouts rather than an automated pipeline. NoBGNow removes backgrounds in the browser, so the file is never uploaded; the trade-off is one image at a time and no programmatic endpoint.