HiAPI
Blog

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

Oct 31, 2026 8 min read
深墨绿底上,两组宽扁丝带以平纹编织方式交错成开放式网格,穿插克制的黄铜色与深松绿色条带,为教程文章封面图。

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 identifier seedream-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 timeout on 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 →

Start building with persistent AI artifacts

Get your API key and call image, video and audio models through one task-based API.

Read the API docs