API

Everything the page does is available over plain HTTP. No keys, no accounts, nothing stored. One difference: the page runs the model in your browser, the API runs it on the server. So the photo is uploaded, held in memory for the length of the request, and gone when the response is sent. If that matters for a photo, use the page, which never uploads.

Plain curl

Send the bytes, get the cutout back. Options travel in the query string. Terminal clients get errors as one line of text; everything else gets JSON.

# Raw body in, PNG with a transparent backdrop out
curl --data-binary @photo.jpg -H 'Content-Type: image/jpeg' https://rmbg.xditya.me/api/v1/remove -o photo-rmbg.png

# A form works too; with ?download its filename names the file
curl -F image=@photo.jpg https://rmbg.xditya.me/api/v1/remove -o photo-rmbg.png

# Flat backdrop: white, black, or any hex colour
curl --data-binary @photo.jpg -H 'Content-Type: image/jpeg' 'https://rmbg.xditya.me/api/v1/remove?bg=white' -o photo-white.png
curl --data-binary @photo.jpg -H 'Content-Type: image/jpeg' 'https://rmbg.xditya.me/api/v1/remove?bg=1a2b3c' -o photo-navy.png

# The original, blurred, behind the subject; smaller as WebP
curl --data-binary @photo.jpg -H 'Content-Type: image/jpeg' 'https://rmbg.xditya.me/api/v1/remove?bg=blur&format=webp' -o photo-blur.webp

# Ask for a download header, so a browser or wget picks the file name
curl -OJ --data-binary @photo.jpg -H 'Content-Type: image/jpeg' 'https://rmbg.xditya.me/api/v1/remove?download&name=portrait'

Request

One endpoint. The body is the image, raw or in a form. PNG, JPEG, WebP, GIF, AVIF and TIFF are read; the type is sniffed from the bytes, so the Content-Type header is a courtesy, not a contract.

POST /api/v1/remove
Content-Type: image/jpeg               raw bytes in the body
Content-Type: multipart/form-data      field "image" (or "file"); options may be fields too

Options (query string, or form fields)
  bg        transparent          default; PNG with alpha
            white | black        flat backdrop
            1a2b3c | #fff        any hex colour, with or without the #
            blur                 the original, blurred, behind the subject
  format    png | webp           default png; webp is quality 92
  download  (flag)               adds Content-Disposition: attachment; filename="<stem>-rmbg.png" (.webp for webp)
  name      photo                the stem for that file name; without it the multipart file name is used
  plain     (flag)               errors as text/plain, whatever the client

Authorization: Bearer <key>            only on instances that set API_KEY; the public one has no keys

Response

The image bytes, nothing wrapped around them. The size is the input size, unless the photo was scaled down first (see limits).

200 OK
Content-Type: image/png                image/webp with format=webp
Content-Length: 412884
Cache-Control: no-store
X-Engine: onnxruntime-node
X-Duration-Ms: 1840                    model time on the server, for your own timing
X-Image-Size: 1600x1200                width x height of the result
Access-Control-Allow-Origin: *         so a browser on any site can call it
Content-Disposition: attachment; filename="photo-rmbg.png"     only with ?download

Examples

In a browser. The API allows any origin, so this runs from a page on another domain too. file is a File from an input or a drop; the result is a Blob you can show or save.

const res = await fetch("https://rmbg.xditya.me/api/v1/remove?bg=white", {
  method: "POST",
  headers: { "Content-Type": file.type },
  body: file,
});
if (!res.ok) throw new Error((await res.json()).error.message);
const blob = await res.blob();
img.src = URL.createObjectURL(blob);
console.log(res.headers.get("X-Duration-Ms"), "ms on the server");

In Node 18 or newer, with the built-in fetch. Nothing to install.

import { readFile, writeFile } from "node:fs/promises";

const photo = await readFile("photo.jpg");
const res = await fetch("https://rmbg.xditya.me/api/v1/remove?format=webp", {
  method: "POST",
  headers: { "Content-Type": "image/jpeg" },
  body: photo,
});
if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message}`);
}
await writeFile("photo-rmbg.webp", Buffer.from(await res.arrayBuffer()));

In Python, with requests. A multipart upload, so the file name rides along.

import requests

with open("photo.jpg", "rb") as f:
    r = requests.post(
        "https://rmbg.xditya.me/api/v1/remove",
        params={"bg": "blur"},
        files={"image": ("photo.jpg", f, "image/jpeg")},
        timeout=60,
    )

if r.status_code == 429:
    print("rate limited, retry in", r.headers.get("Retry-After"), "s")
r.raise_for_status()
with open("photo-rmbg.png", "wb") as out:
    out.write(r.content)

A folder at a time. The loop below writes name-rmbg.png next to every jpg and keeps going when one fails. Ten a minute is the limit on the public instance, so the sleep stays under it.

for f in *.jpg; do
  curl -sS --fail --data-binary "@$f" -H 'Content-Type: image/jpeg' \
    'https://rmbg.xditya.me/api/v1/remove?bg=white' -o "${f%.jpg}-rmbg.png" \
    && echo "done  $f" || echo "failed  $f"
  sleep 6
done

Limits & errors

  • One image per request, up to 12 MB. Bigger bodies get 413: at once when the Content-Length says so, else the moment the stream passes the limit. (The page takes up to 25 MB, because it never uploads.)
  • Photos over 4,096 px on the long side are scaled down first, so the result is at most that size. Over 40 megapixels they are refused with 413 instead, before the model sees them. EXIF rotation is applied; the result is upright.
  • 10 requests a minute per IP. Over that you get 429 with a Retry-After header, in seconds. The limit counts requests, not successes; a 415 costs the same as a cutout.
  • A run takes two to five seconds on the server for a typical photo, and the server caps a request at 60 seconds. At most 2 run at once per instance; a burst queues, and when the queue is over 8 deep you get 503 with Retry-After: 5 rather than a long wait.
  • Errors are { "error": { "code", "message" } } with the matching status. Clients that look like a terminal (curl, wget, httpie, xh) get error: message (code) as text instead; force either with Accept: application/json, Accept: text/plain or ?plain. Every error also carries the code in an X-Error-Code header.
400  invalid            the body could not be decoded, or bg / format is not one of the listed values
401  unauthorized       the instance wants a key and the Bearer token is missing or wrong
413  too_large          more than 12 MB, or more than 40 megapixels
415  unsupported_type   the bytes are not an image the server can read
429  rate_limited       over 10 a minute from this IP; Retry-After says when
503  busy               the queue is full; Retry-After: 5
500  engine             the model didn't answer; try again in a moment
500  internal_error     something else went wrong; try again

What the server keeps

Nothing of the photo. It is decoded in memory, run through the model, composited, and streamed back; it is never written to disk, never cached, and its bytes are never logged. A failure logs one line with the error class, not the input. The only state that outlives a request is the rate limiter: a counter per IP (per /64 for IPv6) that expires after a minute, in the process's memory by default, or in Redis when the operator configured one.

No analytics, no third-party scripts, no cookies. Responses carry Cache-Control: no-store, pages carry Referrer-Policy: no-referrer and a nonce-based Content Security Policy that lets the page talk only to itself and the model CDN. That is what the code does; it is not a promise about a network between you and the server. For a photo that must not leave your machine, the page is the answer: it runs the same model in the browser and uploads nothing.

Self-host

It is a plain Next.js app; the API is a route in it. Every variable is optional. The model weights (44 MB) are fetched once from imgly's CDN on the first request, checked against a pinned sha256, and cached under the system temp directory, so a warm instance answers in seconds. A mirror is checked chunk by chunk against its own manifest, and against RMBG_MODEL_SHA256 when that is set.

API_KEY=                 lock the API: requests then need Authorization: Bearer <key>
RATE_LIMIT_PER_MIN=10    requests per minute per IP
DISABLE_RATE_LIMIT=1     no limiter at all (tests, a box behind your own auth)
UPSTASH_REDIS_REST_URL=  share the limiter across instances (Upstash; KV_REST_API_URL works too)
UPSTASH_REDIS_REST_TOKEN=
TRUSTED_PROXY_HOPS=1     proxies in front of the app that append X-Forwarded-For; 0 on Vercel, 1 elsewhere by default
RMBG_MODEL_URL=          base URL for the weights (a mirror, or an air-gapped copy of the CDN layout)
RMBG_MODEL_SHA256=       with a mirror, the sha256 the assembled model must hash to; the CDN's file is pinned in config
NEXT_PUBLIC_SITE_URL=    the public origin, for the URLs on this page

On Vercel the route declares a 60 second duration, which every plan allows: functions may run 300 seconds with Fluid Compute (on by default for new projects), and 60 on Hobby without it. A run needs two to five of those. The platform caps request bodies at 4.5 MB, below the 12 MB the route allows, and answers its own 413 for bigger uploads. The ONNX runtime and sharp are loaded from node_modules at run time rather than bundled; the function ships the linux binding plus libonnxruntime.so.1 (about 45 MB with sharp) through outputFileTracingIncludes in next.config.ts, well under the 250 MB size limit.

Capabilities

What this instance allows, as JSON. Cached for a minute.

GET https://rmbg.xditya.me/api/v1/info

{
  "name": "rmbg", "version": "…",
  "engine": "onnxruntime-node", "model": "isnet_quint8",
  "limits": { "maxBytes": 12582912, "maxEdge": 4096, "maxPixels": 40000000 },
  "backdrops": ["transparent","white","black","#rrggbb","blur"],
  "formats": ["png","webp"],
  "rateLimit": { "perMinute": 10 },     null when the instance has no limit
  "auth": "none"                       "bearer" on instances that set API_KEY
}

no keys · nothing stored · source