Remove image backgrounds in Python or Node.js
Working code you can paste into a project, including the parts most examples skip: what to do when you hit the rate limit, when you run out of credits, and when a render takes longer than the request is willing to wait.
1. Get a key
Create an account, then generate a key from your dashboard. New accounts start with 3 credits and previews are always free, so you can build the whole integration without spending anything.
Keep the key server-side. Anyone holding it can spend your credits.
Pass it as Authorization: Bearer … or as
X-Api-Key — both work.
2. The smallest thing that works
Python
# pip install requests import requests with open("photo.jpg", "rb") as f: r = requests.post( "https://cutmeout.uk/v1/removebg", headers={"Authorization": "Bearer cmo_live_…"}, files={"image_file": f}, data={"size": "full"}, timeout=120, ) r.raise_for_status() with open("cutout.png", "wb") as out: out.write(r.content)
Node.js
// Node 18+ — fetch, FormData and Blob are all built in. import { readFile, writeFile } from "node:fs/promises"; const fd = new FormData(); fd.append("image_file", new Blob([await readFile("photo.jpg")]), "photo.jpg"); fd.append("size", "full"); const res = await fetch("https://cutmeout.uk/v1/removebg", { method: "POST", headers: { Authorization: "Bearer cmo_live_…" }, body: fd, }); if (!res.ok) throw new Error(`HTTP ${res.status}`); await writeFile("cutout.png", Buffer.from(await res.arrayBuffer()));
3. The three responses that will bite you
The snippets above are fine for a one-off script. For anything running unattended, handle these:
| Status | What happened | What to do |
|---|---|---|
| 202 | The render outlasted our 80-second synchronous ceiling. The body is JSON with a
poll URL — not image bytes. |
Poll the URL until it returns the image |
| 429 | Over 60 requests a minute. The error carries
retry_after in seconds. |
Sleep for that long, then retry |
| 402 | Out of credits. | Stop — retrying will not help |
Every successful response also carries X-Credits-Charged,
X-Job-Id, X-Width and
X-Height, plus X-RateLimit-Remaining so you
can throttle yourself before we do it for you.
4. A version you can leave running
Python
import time, requests API = "https://cutmeout.uk/v1/removebg" KEY = "cmo_live_…" HEADERS = {"Authorization": f"Bearer {KEY}"} class OutOfCredits(Exception): pass def cutout(path, size="full", fmt="png", attempts=4): """Returns the finished image bytes, or raises.""" for attempt in range(attempts): with open(path, "rb") as f: r = requests.post(API, headers=HEADERS, files={"image_file": f}, data={"size": size, "format": fmt}, timeout=120) if r.status_code == 200: return r.content # Queued rather than finished: poll the job instead of giving up. if r.status_code == 202: return _poll(r.json()["poll"]) if r.status_code == 402: raise OutOfCredits(r.json()["errors"][0]["title"]) if r.status_code == 429: wait = int(r.json()["errors"][0].get("retry_after", 5)) time.sleep(wait) continue # 5xx is worth one more go; 4xx means the request itself is wrong. if r.status_code < 500: raise RuntimeError(r.json()["errors"][0]["title"]) time.sleep(2 ** attempt) raise RuntimeError("gave up after retries") def _poll(url, timeout=600): deadline = time.time() + timeout while time.time() < deadline: r = requests.get(url, headers=HEADERS, timeout=60) if r.status_code == 200 and not r.headers["content-type"].startswith("application/json"): return r.content time.sleep(3) raise TimeoutError(url) open("cutout.png", "wb").write(cutout("photo.jpg"))
Node.js
import { readFile, writeFile } from "node:fs/promises"; const API = "https://cutmeout.uk/v1/removebg"; const KEY = "cmo_live_…"; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); async function cutout(path, { size = "full", format = "png", attempts = 4 } = {}) { for (let attempt = 0; attempt < attempts; attempt++) { const fd = new FormData(); fd.append("image_file", new Blob([await readFile(path)]), path); fd.append("size", size); fd.append("format", format); const res = await fetch(API, { method: "POST", headers: { Authorization: `Bearer ${KEY}` }, body: fd, }); if (res.status === 200) return Buffer.from(await res.arrayBuffer()); // Queued rather than finished. if (res.status === 202) return poll((await res.json()).poll); const { errors } = await res.json(); if (res.status === 402) throw new Error(`Out of credits: ${errors[0].title}`); if (res.status === 429) { await sleep((errors[0].retry_after ?? 5) * 1000); continue; } if (res.status < 500) throw new Error(errors[0].title); await sleep(2 ** attempt * 1000); } throw new Error("gave up after retries"); } async function poll(url, timeoutMs = 600_000) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const r = await fetch(url, { headers: { Authorization: `Bearer ${KEY}` } }); if (r.ok && !r.headers.get("content-type").startsWith("application/json")) { return Buffer.from(await r.arrayBuffer()); } await sleep(3000); } throw new Error(`timed out polling ${url}`); } await writeFile("cutout.png", await cutout("photo.jpg"));
5. Processing a folder
For bulk work, pass async=1. You get a job id back straight away
instead of holding a connection open for every image, which lets you queue the whole batch and
collect the results afterwards.
import pathlib, requests # 1. Queue everything. Each POST returns immediately with an id. jobs = {} for p in pathlib.Path("photos").glob("*.jpg"): with open(p, "rb") as f: r = requests.post(API, headers=HEADERS, files={"image_file": f}, data={"size": "full", "async": "1"}, timeout=60) r.raise_for_status() jobs[p.stem] = r.json()["poll"] # 2. Collect them. Renders take ~10-15s each, so this is where the time goes. for stem, url in jobs.items(): open(f"out/{stem}.png", "wb").write(_poll(url))
Stay under 60 requests a minute while queueing, and watch
X-RateLimit-Remaining if you are close to it.
6. Controlling what you spend
| Parameter | Values | Cost |
|---|---|---|
| size | preview (max 640px) or
full |
preview is free;
full costs 1 credit |
| format | png, jpg,
webp |
No difference |
| bg_color | A hex colour such as #ffffff |
No difference — omit it for transparency |
Running size=preview in development costs nothing, so build and test
the whole integration for free and only switch to full when you ship.
Ready to build?
The full API reference covers every parameter, response header and error code. Coming from another service? See the migration notes.
Get an API key