How to swap or remove image backgrounds on hiapi using image-to-image models — minimal code, batch patterns, and the schema details that keep product photos framed exactly right.

Traditional background removal tools give you exactly one operation: cut the subject out and leave transparency (or a flat fill) behind. Prompt-based background removal with an image-to-image model gives you the cutout and the replacement in a single call — a seamless white studio backdrop, a lifestyle scene, a solid brand color — because the model is regenerating the background pixel-by-pixel around a preserved subject instead of just computing an alpha matte.
This guide shows exactly how to do that on hiapi: which image-to-image models actually handle background removal well, the request/response shapes you need to code against, and the production patterns (callbacks, retries, error handling) that keep a batch job from silently corrupting your product catalog.
Prerequisites:
requests installed (pip install requests)Every generation on hiapi — image, video, or audio — goes through the same async task lifecycle: you submit a job, get back a taskId, and either poll for the result or receive it on a callback URL. See the async task API reference for the full contract; the pieces that matter for background removal are:
POST /v1/tasks with model + input (your prompt, source image URL, and output shape) returns {"code":200,"data":{"taskId":"tk-hiapi-..."}}. No status field yet at this point.GET /v1/tasks/{taskId} returns data.status, one of queued, handling, archiving, success, fail. Only success and fail are terminal.success, data.output is an array of {"url","type","expireAt"}. The url is signed and short-lived (expireAt is a Unix timestamp) — download it immediately, don't store the hot link.For the model, use an image-to-image endpoint with a prompt that describes the replacement background, not "remove background" in isolation — these models respond much better to a concrete target than to a negative instruction:
Remove the background completely and replace it with a seamless, pure white
studio background. Keep the subject's shape, edges, lighting, and colors
exactly unchanged. Do not add shadows, reflections, or any other objects
that were not in the original image.
seedream-5.0-lite/image-to-image is the cheapest capable option for this — it takes prompt, image_urls, aspect_ratio, and resolution (2K or 4K only; there's no 1K tier on this endpoint, and unknown fields like n or seed are rejected outright with a 400).
import os
import time
import requests
API = "https://api.hiapi.ai/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['HIAPI_API_KEY']}",
"Content-Type": "application/json",
}
BACKGROUND_PROMPT = (
"Remove the background completely and replace it with a seamless, pure "
"white studio background. Keep the subject's shape, edges, lighting, and "
"colors exactly unchanged. Do not add shadows, reflections, or any other "
"objects that were not in the original image."
)
def create_task(model: str, task_input: dict) -> str:
resp = requests.post(f"{API}/tasks", headers=HEADERS,
json={"model": model, "input": task_input}, timeout=60)
resp.raise_for_status()
return resp.json()["data"]["taskId"]
def wait_task(task_id: str, timeout_s: int = 180, poll_s: int = 3) -> dict:
deadline = time.time() + timeout_s
while time.time() < deadline:
resp = requests.get(f"{API}/tasks/{task_id}", headers=HEADERS, timeout=30)
resp.raise_for_status()
task = resp.json()["data"]
if task["status"] == "success":
return task
if task["status"] == "fail":
err = task.get("error") or {}
raise RuntimeError(f"task {task_id} failed: {err.get('code')} {err.get('message')}")
time.sleep(poll_s)
raise TimeoutError(f"task {task_id} still {task['status']} after {timeout_s}s")
task_id = create_task("seedream-5.0-lite/image-to-image", {
"prompt": BACKGROUND_PROMPT,
"image_urls": ["https://your-cdn.example.com/product-photo.jpg"],
"aspect_ratio": "1:1",
"resolution": "2K",
})
task = wait_task(task_id)
image_bytes = requests.get(task["output"][0]["url"], timeout=60).content
with open("product-no-bg.png", "wb") as f:
f.write(image_bytes)
That's the whole loop: submit, poll, download. For a quick no-code check before you wire this into a pipeline, the AI Product Photo Lab runs the same kind of background swap in the browser.
Real catalogs are dozens or hundreds of images, and the task API has no native batch endpoint — you fan out individual task submissions yourself and poll them concurrently:
from concurrent.futures import ThreadPoolExecutor, as_completed
def remove_background(image_url: str, aspect_ratio: str = "1:1") -> str:
task_id = create_task("seedream-5.0-lite/image-to-image", {
"prompt": BACKGROUND_PROMPT,
"image_urls": [image_url],
"aspect_ratio": aspect_ratio,
"resolution": "2K",
})
task = wait_task(task_id)
return task["output"][0]["url"]
source_urls = [
"https://your-cdn.example.com/sku-1001.jpg",
"https://your-cdn.example.com/sku-1002.jpg",
# ...
]
results = {}
with ThreadPoolExecutor(max_workers=5) as pool:
futures = {pool.submit(remove_background, url): url for url in source_urls}
for future in as_completed(futures):
url = futures[future]
try:
results[url] = future.result()
except Exception as exc:
results[url] = None
print(f"failed: {url}: {exc}")
Keep max_workers modest — polling too aggressively across many concurrent tasks just burns request quota without speeding up generation, since each task's wall-clock time is dominated by model inference, not your polling frequency.
Preserving exact product framing. For catalog work you usually can't tolerate the model reframing or slightly cropping the subject — a shoe that's centered in the input needs to stay centered in the output. seedream-5.0-pro/image-to-image supports an aspect_ratio value the lite tier doesn't: match_input_image, which locks the output canvas to the input image's own dimensions instead of forcing it into a fixed ratio. It also accepts up to 10 image_urls per call and caps resolution at 1K/2K (a different resolution set than lite — don't assume schema parity across seedream endpoints, each one has its own enum). For pure background-removal-and-replacement work where framing has to match exactly, this is the tier to reach for, at a higher per-image cost — see pricing for current rates.
If you need to reference more than one source angle at once (e.g. front and side shots of the same product) instead of processing one image per call, nano-banana-2 is a reasonable alternative — it takes reference images through image_input (not image_urls) plus a resolution of 1K/2K/4K.
Callbacks instead of polling. For a batch of any real size, a webhook beats polling: add a callback object as a sibling of input — not nested inside it, POST /v1/tasks rejects that shape:
task_input = {
"prompt": BACKGROUND_PROMPT,
"image_urls": [image_url],
"aspect_ratio": "1:1",
"resolution": "2K",
}
resp = requests.post(f"{API}/tasks", headers=HEADERS, json={
"model": "seedream-5.0-lite/image-to-image",
"input": task_input,
"callback": {"url": "https://your-service.example.com/hooks/hiapi", "when": "final"},
}, timeout=60)
hiapi POSTs the same data object GET /v1/tasks/{taskId} would return to your callback URL once the task reaches a terminal state. See the async task reference for the optional X-HiAPI-Signature HMAC header if you want to verify the callback's authenticity server-side.
Idempotency. There's no documented idempotency_key field on POST /v1/tasks — build idempotency at the application layer instead. Key a small store (a database row, a Redis key, even a local file for a one-off script) by a hash of (image_url, prompt, model), and before submitting a new task, check whether that key already has a stored taskId or output URL. If your batch job crashes halfway through and reruns, you skip re-billing images you've already processed instead of quietly duplicating cost.
Error handling — three distinct shapes, don't treat them the same:
{"code":400,"error_code":"INVALID_REQUEST","message":"...","data":null}. This means your input dict doesn't match the model's schema — check for a typo'd field name or wrong enum value.{"error":{"code":"permission_denied","message":"...","request_id":"...","type":"hiapi_error"}}. A missing/expired API key, not a bad request — don't retry, fix the key.data.status == "fail" with data.error describing what went wrong. This is retryable; a transient upstream error can succeed on resubmission.if task["status"] == "fail":
err = task.get("error") or {}
# log err["code"] / err["message"], then decide: retry, skip, or alert
Don't collapse all three into one except Exception — a 401 retried in a loop just burns your rate limit without ever succeeding, while a task-level fail genuinely might succeed on a second attempt.
Does this actually remove the background, or just paint over it? Functionally both — the model regenerates the entire canvas around the preserved subject, so there's no leftover matte or fringing the way there can be with edge-detection cutout tools. The tradeoff is that "keep everything else identical" has to be stated explicitly in the prompt, or the model will happily restyle the subject too.
Can I get a transparent PNG instead of a solid-color background? Ask for it directly in the prompt (e.g. "replace the background with transparency" or "isolate the subject on a transparent background") and request PNG output where the model supports an output_format field — support varies by model, so check the specific model's page before assuming it's available.
Why does my image come back reframed instead of matching the original? You're likely on a tier whose aspect_ratio enum doesn't include match_input_image — on lite/4.5 tiers you're picking a fixed ratio, which can crop or letterbox relative to your source. Switch to seedream-5.0-pro/image-to-image and set aspect_ratio to match_input_image.
How much does this cost per image? Pricing differs per model and resolution tier and changes over time — check pricing rather than hardcoding a number in your own code or docs.
What if my image URL isn't public yet? The task API fetches by URL server-side, so it has to be reachable without auth. Upload to any public bucket or CDN path first — a signed/private URL or localhost path will fail at task creation or (worse) fail silently mid-processing.
Key Takeaways