Removing a background from an image URL sounds cleaner than uploading a file. You already have a URL, so why not hand that URL to the API and let the processor fetch it? Often, that is exactly the right move. But URL input moves the hardest part of the request from "can I upload bytes?" to "can a server fetch this exact URL right now?"

That difference matters. A URL that works in your signed-in browser may fail from a server. A CDN preview page may return HTML instead of an image. A signed S3 URL may expire before the worker uses it. A path containing spaces or parentheses may break if some client decodes and re-encodes it after signing.

This guide uses the BackgroundErase API examples, but the same design applies to any background-removal API that accepts remote image URLs. For brevity, we'll skip the basic "what is a URL" fluff and focus on the production decisions: reachability, request mode, signed URLs, response handling, retries, and when to switch back to image_file.

Core rule

Send URLs only when the API can fetch the raw image directly.

  • The URL must return a direct image response, not an HTML preview page.
  • The API server must be able to fetch it without cookies or app sessions.
  • Signed URLs need enough lifetime for queue delay, retries, and processing.
  • Multipart URL requests are the clean path when you want output image bytes.
  • Remote fetch failures should usually fail fast, not retry forever.

01

When URL input is right

URL input is best when the source image already lives somewhere your backend can point to: a CDN, Shopify media URL, supplier image URL, S3 presigned URL, GCS signed URL, R2 object URL, or a stable public asset from a CMS. In those cases, sending image_url avoids downloading the file into your app just to upload it again (this is the ordinary setup using image_file).

Decision shortcut

Use image_url when:
  - the source already has a stable CDN or object-storage URL
  - your worker does not need to download and re-upload the file
  - the API can fetch the exact URL without cookies or app auth

Use image_file when:
  - the source is private, short-lived, or session-dependent
  - you already have the bytes in your backend
  - URL fetch failures would be harder to debug than direct upload
01

Stable source URL

Good for public CDN URLs, long-lived object URLs, or signed URLs created immediately before the API call.

02

Server-readable

The API fetches the URL from its own network context. Browser cookies, internal VPN access, and admin sessions do not count.

03

Direct image body

The URL should return image/jpeg, image/png, image/webp, or another supported image type.

04

Traceable failure

Store enough source metadata that a failed job can tell you whether the URL returned 403, 404, HTML, or timed out.

If the image is already in your backend as bytes, direct multipart image_file upload is usually less fragile. URL mode is not automatically more production grade. It is production grade when the URL itself is a durable input.

02

The request shape

The simplest production shape is a multipart request with image_url as a normal form field. You still send output options like format, channels, size, crop, and bg_color alongside it if you need additional options.

cURL URL input

curl -f "https://api.backgrounderase.com/v2" \
  -H "x-api-key: YOUR_API_KEY" \
  -F "image_url=https://cdn.example.com/products/sku-123.jpg" \
  -F "format=png" \
  -F "channels=rgba" \
  -F "size=full" \
  -o sku-123-transparent.png

In a worker, use FormData the same way you would for a file upload. The difference is that the API downloads the source image from the URL before processing it.

Node.js worker

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

const apiKey = process.env.BACKGROUNDERASE_API_KEY;
const sourceUrl = "https://cdn.example.com/products/sku-123.jpg";

const form = new FormData();
form.append("image_url", sourceUrl);
form.append("format", "png");
form.append("channels", "rgba");
form.append("size", "full");

const response = await fetch("https://api.backgrounderase.com/v2", {
  method: "POST",
  headers: { "x-api-key": apiKey },
  body: form,
});

if (!response.ok) {
  throw new Error(await response.text());
}

const contentType = response.headers.get("content-type") || "";
if (!contentType.toLowerCase().startsWith("image/")) {
  throw new Error(`Expected image bytes, got ${contentType || "unknown content type"}`);
}

await writeFile("./sku-123-transparent.png", Buffer.from(await response.arrayBuffer()));
Source URL API fetch Background removal Output image

For a transparent PNG, use format=png and channels=rgba. For a marketplace-ready white background image, use a flattened format like jpg plus bg_color=#ffffff. The URL chooses the input. The output flags choose the result.

03

Preflight the source URL

The most useful debugging habit is testing the exact URL before you blame the processor. Not the product page. Not the image as it appears in your admin UI. The exact string you will send as image_url.

URL preflight

curl -L --fail --output /dev/null \
  --write-out "status=%{http_code}\ncontent_type=%{content_type}\nfinal_url=%{url_effective}\n" \
  "https://cdn.example.com/products/sku-123.jpg"

A good URL preflight answers four questions: did it return a 200, did redirects end somewhere sensible, did the body claim to be an image, and is the URL reachable without your application cookies? A browser tab is not a sufficient test because your browser may be carrying auth state the API will never have.

Node.js preflight

const response = await fetch("https://cdn.example.com/products/sku-123.jpg", {
  redirect: "follow",
});

console.log("Status:", response.status);
console.log("Content-Type:", response.headers.get("content-type"));
console.log("Final URL:", response.url);

if (!response.ok) {
  throw new Error(`URL fetch failed with ${response.status}`);
}

const contentType = response.headers.get("content-type") || "";
if (!contentType.toLowerCase().startsWith("image/")) {
  throw new Error(`URL did not return an image: ${contentType || "unknown"}`);
}

HEAD requests are useful but not perfect. Some image hosts do not support HEAD correctly. If HEAD is weird, test with a GET that follows redirects and discards the body.

04

Signed and private URLs

Signed URLs are often the right compromise for private object storage. You keep the bucket private, generate temporary read access for one object, and send that temporary URL to the API. The important word is temporary. The URL needs to survive queue delay, API fetch time, retries, and clock skew.

S3 signed URL shape

import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const s3 = new S3Client({ region: "us-east-1" });

const imageUrl = await getSignedUrl(
  s3,
  new GetObjectCommand({
    Bucket: "my-private-bucket",
    Key: "incoming/sku-123 (front).jpg",
  }),
  { expiresIn: 15 * 60 },
);

const form = new FormData();
form.append("image_url", imageUrl);
form.append("format", "png");
form.append("channels", "rgba");

Generate late

Create the signed URL as close as possible to the API call, not when the user first uploads the image hours earlier.

Leave expiry headroom

A 30-second URL is fragile. Use enough lifetime for normal network variance and one bounded retry.

Preserve the exact string

Do not decode and rebuild signed URL paths or query strings. Spaces, plus signs, parentheses, and percent escapes can matter.

Compare direct fetch

If a direct GET works but the API returns 403, inspect whether some client or proxy changed the signed URL before fetch.

If signed URL failures are frequent, switch the architecture: have your backend download the private object and send image_file, or process from a queue where the worker can create fresh signed URLs immediately before each attempt.

05

Handle response modes

Multipart requests generally return raw image bytes. That means your worker should check HTTP status, confirm an image Content-Type, and write the response body directly to disk or object storage.

JSON mode is different. It is useful when the platform you are integrating with expects JSON, but you should parse the response as JSON and decode the base64 field. Do not mix the two response paths.

JSON response path

const response = await fetch("https://api.backgrounderase.com/v2", {
  method: "POST",
  headers: {
    "x-api-key": process.env.BACKGROUNDERASE_API_KEY,
    "content-type": "application/json",
  },
  body: JSON.stringify({
    image_url: "https://cdn.example.com/products/sku-123.jpg",
    format: "jpg",
    bg_color: "#ffffff",
    size: "hd",
  }),
});

if (!response.ok) {
  throw new Error(await response.text());
}

const body = await response.json();
const output = Buffer.from(body.image, "base64");
await writeFile("./sku-123-white.jpg", output);

Job record

{
  "id": "imgjob_01j2",
  "sourceUrl": "https://cdn.example.com/products/sku-123.jpg",
  "sourceUrlCheckedAt": "2026-07-07T14:20:00.000Z",
  "sourceContentType": "image/jpeg",
  "outputFormat": "png",
  "channels": "rgba",
  "status": "ready",
  "resultKey": "processed/sku-123-transparent.png",
  "attempts": 1
}

Store the original source URL, the checked content type, the output flags, and the result key. If an output looks wrong later, you need to know whether the URL changed, redirected, expired, or returned a different image than the one the user saw.

06

Failure handling

URL failures are not all retryable. A timeout or upstream 5xx from a remote host may deserve a bounded retry. A 403, 404, expired signature, login page, or HTML response usually needs a different URL or a direct upload path.

403 forbidden

The object is private, the signature expired, hotlink protection blocked the request, or the signed URL was changed after signing.

404 not found

The stored URL is stale, the object moved, or the system saved a derived preview path instead of the original file path.

200 with HTML

The URL points to a preview page, login page, CDN error page, or image viewer rather than the raw image bytes.

Timeout

The remote host is slow, rate limiting, blocking server fetches, or serving a file that is too large for practical URL mode.

Invalid image body

The API fetched something, but the bytes did not decode as a supported image. Check content type and actual body bytes.

Repeated rate limits

Lower worker concurrency and retry with jitter. Do not hammer the API with the same bad URL after a permanent fetch failure.

The retry rule I prefer: retry transient network and service errors; fail fast on permanent URL accessibility problems. If the source URL itself is bad, retries just add cost and make the logs harder to read.

07

Production checklist

A good image URL pipeline is boring because every URL has already proven it can be fetched, every response mode has one handler, and every failure tells you whether to retry or fix the source.

Input

Send exactly one primary source: image_url, image_file, or base64. Avoid ambiguous fallback behavior.

Reachability

Preflight the exact URL from a server context. Require 200, image content type, and sensible redirect behavior.

Signing

Generate signed URLs late, give them enough lifetime, and do not re-encode the signed path or query string.

Output

Use multipart URL requests for raw image bytes. Use JSON mode only when your integration wants base64 JSON.

Retries

Retry timeouts and 5xx errors with limits. Fail fast on 403, 404, HTML pages, and known invalid image bodies.

Storage

Store the source URL, checked metadata, output options, result key, attempt count, and final processor status.

Treat the URL as part of the input, not just a pointer.

Image URL mode is excellent when the URL is stable, direct, and server-readable. When it is private, session-bound, or hard to keep alive, send the bytes instead. The boring path is usually the reliable one.

Read the API docs
Jack Spruyt

Written by

Jack Spruyt

Cofounder at BackgroundErase

Jack leads product strategy, technology, and growth at BackgroundErase.