CutMeOut
Quickstart

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:

StatusWhat happenedWhat 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

ParameterValuesCost
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