Tutorial
How to Call seedream-4.5/image-to-image via the HiAPI API: curl and Python
A working request walkthrough with multi-reference inputs, async polling, and output handling
What this endpoint does and when to choose it
seedream-4.5/image-to-image turns one or more reference images plus a text prompt into a new image. It belongs to the image-to-image family on HiAPI: you supply visual context, and the model reinterprets it according to your instructions. That is the key difference from text-to-image, where the prompt is the only input and the model invents everything.
Choose image-to-image when the visual outcome should stay anchored to something you already have: restyling an existing asset, preserving a product's shape and layout while changing lighting or background, iterating on a composition without losing the subject, or enforcing a consistent look across a batch by reusing the same references.
Choose text-to-image when you have no reference material and just want a scene described in words.
The endpoint is asynchronous. You submit a job, receive an identifier, and poll until the job reaches a terminal state before fetching the output. Plan for that lifecycle from the start; the rest of this guide builds one working request and then hardens it.
Building a working curl request
Every request to this endpoint shares the same anatomy. Work through the fields one at a time.
Endpoint and method
Requests are POST to the image-to-image route under the HiAPI base URL, using the model identifier seedream-4.5/image-to-image. The model name is a value inside the payload, not part of the path.
Authentication
Pass your HiAPI API key in the Authorization header as a bearer token. Keep it in an environment variable so it never lands in shell history or source control:
export HIAPI_API_KEY="your-key-here"
The minimal payload
curl -sS -X POST "$HIAPI_BASE_URL/v1/images/generations" \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "seedream-4.5/image-to-image",
"prompt": "restyle this photo as a warm studio portrait",
"image": "https://example.com/reference.jpg"
}'
Field by field:
model— the literal identifierseedream-4.5/image-to-image.prompt— what to do with the reference. Describe the transformation, not just the subject.image— the reference input. For a single image this is one value; the next section covers multiple references.
Reading the first response
The call returns an accepted job, not the finished image. Capture the job identifier from the response body and keep it; you will need it for polling. Do not treat a 200-class status as "image ready".
Run this exact request once before writing any client code. A payload that fails in curl will fail in Python for the same reason, and debugging is faster at the shell.
Porting the request to Python
The Python version keeps the payload byte-for-byte identical and swaps only the transport and auth handling. That separation is the whole point: if curl works, Python should work on the first try.
import os
import requests
BASE_URL = os.environ["HIAPI_BASE_URL"]
API_KEY = os.environ["HIAPI_API_KEY"]
payload = {
"model": "seedream-4.5/image-to-image",
"prompt": "restyle this photo as a warm studio portrait",
"image": "https://example.com/reference.jpg",
}
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
resp = requests.post(
f"{BASE_URL}/v1/images/generations",
json=payload,
headers=headers,
timeout=30,
)
resp.raise_for_status()
job = resp.json()
print(job["id"])
Notes that save time later:
- Build the payload as a Python dict and let
json=serialize it. Hand-writing JSON strings is how quoting bugs creep in. - Keep auth construction in one helper. If the header format ever changes, you edit one place.
- Always pass an explicit
timeouton the submission call too. A hung socket on the submit step is indistinguishable from a slow model. - Store the returned job identifier immediately rather than re-deriving it from logs.
The polling code in the next sections uses the same BASE_URL, API_KEY, and headers, so keep them in module scope or a small client class.
Passing multiple reference images
Multi-reference inputs are where image-to-image requests most often go wrong, usually because the model receives three images and a prompt that only describes one of them.
Structure the inputs explicitly
Pass references as a list under the same image field, in the order you intend, and make the prompt acknowledge each position:
payload = {
"model": "seedream-4.5/image-to-image",
"prompt": (
"Use the first reference for the subject's pose, "
"the second for the background setting, "
"and the third for color grading. Keep the subject sharp."
),
"image": [
"https://example.com/pose.jpg",
"https://example.com/background.jpg",
"https://example.com/grading.jpg",
],
}
Why labeled beats positional
The list order is the contract, but the prompt is what tells the model how to read that order. A bare array plus a vague prompt forces the model to guess which reference wins when they conflict. Naming each image's role in the prompt turns an implicit ordering assumption into an explicit instruction.
Practical rules:
- Keep reference count small and each image purposeful. Extra references dilute rather than enrich.
- State conflict resolution: if pose and background references disagree on framing, say which one takes priority.
- Use stable, publicly reachable URLs, or the equivalent upload mechanism. A reference that 404s mid-job fails the whole request.
If the output ignores one reference, the usual fix is not a new model call but a clearer prompt sentence naming that image's role.
Handling the async job lifecycle
Image generation takes time, so the endpoint is job-based. Treat the lifecycle as four discrete steps and code each one.
1. Submit
Send the POST shown earlier. Persist the job identifier from the response before doing anything else.
2. Poll
Issue GET requests against the job route with the same auth headers until the status is terminal:
import time
def poll_job(job_id, interval=2.0, max_interval=30.0, deadline=300.0):
start = time.monotonic()
while True:
r = requests.get(
f"{BASE_URL}/v1/images/generations/{job_id}",
headers=headers,
timeout=30,
)
r.raise_for_status()
data = r.json()
status = data["status"]
if status in ("succeeded", "failed"):
return data
if time.monotonic() - start > deadline:
raise TimeoutError(f"job {job_id} not finished within {deadline}s")
time.sleep(interval)
interval = min(interval * 2, max_interval)
3. Detect terminal states
Only two outcomes end the loop: success, or failure. Anything else means keep waiting. Never assume instant output, and never poll a fixed number of times without a wall-clock deadline.
4. Fetch the result
Handled in the next section.
Backoff matters. A tight sleep(0.1) loop spends your rate budget on status checks. Starting near a couple of seconds and doubling up to a ceiling keeps latency low without flooding the API.
Retrieving and storing the output
When polling returns a success state, the response carries the output reference. What comes back is a pointer to the generated image, not necessarily the bytes themselves.
Download deliberately
data = poll_job(job_id)
url = data["output"][0]["url"] if isinstance(data["output"], list) else data["output"]
with requests.get(url, stream=True, timeout=60) as r:
r.raise_for_status()
with open(f"{job_id}.png", "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
Adjust the path to match the actual response shape you observe from your first real job, rather than assuming a fixed schema.
Storage rules that prevent rework
- Name files by job identifier. It is unique, traceable, and ties the artifact back to the request that produced it.
- Download promptly. Output links may be time-limited, so persist immediately after a successful poll rather than caching URLs for later.
- Stream large files instead of reading them fully into memory.
- Record the prompt and reference list alongside the file. When an output surprises you, the inputs are the debugging record.
If you need deterministic retries, storing the job identifier means you can re-poll or re-download without paying for a second generation.
Errors, retries, and cost control
Most failures fall into one of three buckets, and each wants different handling.
Client errors
Malformed payloads, missing model, an unreachable reference URL, or a bad key. These return immediately and will return again on retry. Fix the request; do not loop on it. Validate that reference URLs resolve before submitting.
Transient server and network errors
Timeouts, connection resets, rate-limit responses. These deserve retries with exponential backoff and jitter, capped by a small maximum attempt count. Retry the specific step — submit, poll, or download — not the entire pipeline. Re-submitting after a successful submit is how you accidentally pay twice.
Job-level failure
A job that reaches a failed terminal state. Retrying the same job identifier will not help. Decide explicitly whether to resubmit with a revised prompt or reference set, and log the failure reason so you can tell the difference between a bad input and a transient fault.
def with_retries(fn, attempts=4, base=1.0):
for i in range(attempts):
try:
return fn()
except requests.RequestException:
if i == attempts - 1:
raise
time.sleep(base * (2 ** i))
Cost control
Billing is tied to generation, so the expensive mistake is duplicate submissions. Enforce a wall-clock deadline on polling, keep submission idempotent by storing job identifiers before polling, and cap concurrency so a retry storm cannot multiply into dozens of billable jobs. Log every submit with its job identifier; that log is your spend audit trail.
Wire these three concerns into the client once, and the same code handles single- and multi-reference requests without change.
Related articles
View all articles →-
Tutorial
How to Call hailuo-2.3-fast/image-to-video via the HiAPI API: curl and Python
A first working request for the fast, low-cost image-to-video tier — from model ID to downloaded MP4
9 min read -
Tutorial
How to Use hailuo-2.3/text-to-video via the HiAPI API: curl, Python, and a Working Request
11 min read -
Tutorial
Kandinsky 6.0 Pro Text-to-Video on HiAPI: curl and Python Tutorial
A request-first walkthrough: build the prompt-to-clip call, submit it, poll for completion, and download the result
11 min read