This recipe shows you how to call flux-2-klein-9b's image-to-image mode through the hiapi unified task API: the exact request shape, a working curl command, a Python script you can drop into a backend, and the one schema quirk that will bite you if you're coming from another model on the platform.
By the end you'll have a script that takes a single reference image URL plus an edit prompt and returns a generated image URL ready to download.
What you need
- An hiapi account and an API key. Grab one from the hiapi dashboard.
- Exactly one reference image hosted at a public HTTPS URL. Unlike some other image-to-image models on the platform that accept multiple references, flux-2-klein-9b's
image_urlsarray is capped at one item — more on that below. - A text prompt describing the edit or transformation.
All requests go to the unified task endpoint with your key in the Authorization header:
POST https://api.hiapi.ai/v1/tasks
Authorization: Bearer sk-<your-key>
Content-Type: application/json
The request shape (and the one field that trips people up)
The model id is flux-2-klein-9b/image-to-image — pass it bare, exactly as listed in /v1/models. Its input schema is strict:
| Field | Type | Required | Notes |
|---|---|---|---|
prompt | string | yes | The edit instruction. |
image_urls | string[] | yes | Exactly one reference image URL — minItems and maxItems are both 1. |
aspect_ratio | string | no | One of 1:1, 4:3, 3:4, 16:9, 9:16. |
seed | integer | no | Fix it to reproduce a result. |
output_format | string | no | One of jpeg, png, webp. |
Two things worth knowing before you write any code:
image_urlstakes exactly one URL, not a range. If you've built shared submission code around a model that accepts 1–14 references, sending two URLs here gets rejected —maxItems: got 2, want 1. This model does single-image edits, not multi-reference blending.- Extra fields are rejected, not ignored.
resolution,size,n,strength,image_size, andnum_images— all common on other image models on the platform — all return400 INVALID_REQUEST: additional properties '<field>' not allowedhere. Don't reuse another model's payload without checking this model's schema first.
Minimal working example: curl
Create the task:
curl -X POST https://api.hiapi.ai/v1/tasks \
-H "Authorization: Bearer sk-<your-key>" \
-H "Content-Type: application/json" \
-d '{
"model": "flux-2-klein-9b/image-to-image",
"input": {
"prompt": "Change the jacket to charcoal grey leather, keep the pose and background unchanged",
"image_urls": ["https://your-cdn.example.com/subject.png"],
"aspect_ratio": "3:4",
"output_format": "png"
}
}'
A successful submission returns a task id inside data:
{"code": 200, "data": {"taskId": "tk-hiapi-..."}}
Poll for the result:
curl https://api.hiapi.ai/v1/tasks/tk-hiapi-... \
-H "Authorization: Bearer sk-<your-key>"
While running, data.status moves through queued → handling → archiving. On completion it flips to "success" and the image appears in data.output:
{
"code": 200,
"data": {
"status": "success",
"output": [{"url": "https://.../result.png", "expireAt": "..."}]
}
}
The output URL is short-lived (note the expireAt). Download the bytes immediately and store them yourself — never hotlink or persist the raw URL.
Production-ready Python: submit, poll, download
import time
import requests
API_BASE = "https://api.hiapi.ai/v1/tasks"
API_KEY = "sk-<your-key>"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
def edit_image(prompt: str, image_url: str,
aspect_ratio: str = "1:1", output_format: str = "png") -> bytes:
# 1. Create the task — image_urls takes exactly ONE url for this model
resp = requests.post(API_BASE, headers=HEADERS, json={
"model": "flux-2-klein-9b/image-to-image",
"input": {
"prompt": prompt,
"image_urls": [image_url],
"aspect_ratio": aspect_ratio,
"output_format": output_format,
},
}, timeout=60)
body = resp.json()
if resp.status_code != 200 or not (body.get("data") or {}).get("taskId"):
raise RuntimeError(f"create failed [{resp.status_code}]: {body}")
task_id = body["data"]["taskId"]
# 2. Poll until terminal state
deadline = time.time() + 600
while time.time() < deadline:
task = requests.get(f"{API_BASE}/{task_id}", headers=HEADERS,
timeout=30).json().get("data") or {}
if task.get("status") == "success":
outputs = task.get("output") or []
if not outputs or not outputs[0].get("url"):
raise RuntimeError(f"task {task_id} succeeded but returned no output URL")
# 3. Download immediately — the URL expires
img = requests.get(outputs[0]["url"], timeout=120)
img.raise_for_status()
return img.content
if task.get("status") == "fail":
err = task.get("error") or {}
raise RuntimeError(f"task failed: {err.get('code')} {err.get('message')}")
time.sleep(5)
raise TimeoutError(f"task {task_id} still running after 600s")
if __name__ == "__main__":
png = edit_image(
"Change the jacket to charcoal grey leather, keep the pose and background unchanged",
"https://your-cdn.example.com/subject.png",
aspect_ratio="3:4",
)
with open("edited.png", "wb") as f:
f.write(png)
print(f"saved edited.png ({len(png)} bytes)")
Reproducible edits with seed
Pass an integer seed and the model will reuse it for that generation, which is useful when you want to A/B test prompt wording while holding everything else constant:
{
"model": "flux-2-klein-9b/image-to-image",
"input": {
"prompt": "Change the jacket to charcoal grey leather, keep the pose and background unchanged",
"image_urls": ["https://your-cdn.example.com/subject.png"],
"seed": 42
}
}
Omit it and the model picks a random seed per call, which is what you want for normal production traffic — vary the prompt, not the seed, when you need different-looking outputs.
Callbacks instead of polling
Polling is fine for scripts and worker queues. For web backends, register a callback when you create the task and let hiapi push the terminal state to you — the callback object sits at the top level of the request, next to model:
{
"model": "flux-2-klein-9b/image-to-image",
"input": { "...": "..." },
"callback": {
"url": "https://your-app.example.com/hooks/hiapi",
"when": "final"
}
}
With "when": "final" you get exactly one POST when the task reaches success or fail. Two rules for the handler:
- Make it idempotent. Key your processing on the task id, so a redelivered webhook doesn't double-process. Store the task id at submission time and treat the callback as "go fetch and finalize," not as the sole source of truth.
- Keep a reconciliation path. If your endpoint was down when the callback fired, a periodic sweep that GETs
/v1/tasks/<id>for unresolved ids catches anything missed.
If your callback never seems to arrive, work through why your hiapi task callback isn't firing — it covers the common failure modes in order of likelihood.
Error handling: the three failures that matter
401 permission_denied at creation — your key can't use this model. The body looks like:
{"error": {"code": "permission_denied", "message": "...", "request_id": "...", "type": "hiapi_error"}}
If you hit this, work through API key is invalid or check the key's model permissions in the dashboard.
400 INVALID_REQUEST at creation — your input doesn't match this model's schema. The message names the offending field directly (e.g. <root>: additional properties 'resolution' not allowed, or image_urls: maxItems: got 2, want 1). These are permanent errors — retrying the same payload will fail forever; fix the field and resend.
status: "fail" on the task — creation succeeded but generation failed (a reference URL the backend couldn't fetch, content policy, upstream capacity). The task's error.code and error.message say which. Unlike 400s, some of these are transient and worth one retry with backoff.
Also worth knowing: a GET for a nonexistent or expired task id returns 404 task not found — treat that as terminal in reconciliation sweeps, not as "still pending."
Where to go next
- flux-2-klein-9b/image-to-image model page — playground and per-image pricing.
- hiapi pricing — current rates across all image models.
- seedream-5.0-lite image-to-image recipe — same task API, a schema that accepts up to 14 reference images instead of 1; a good illustration of why payloads don't transfer between models.
- hiapi async task API docs — full create/poll/callback reference.
- hiapi authentication docs — API key setup and header format.
FAQ
Can I pass more than one reference image to flux-2-klein-9b image-to-image?
No. image_urls has both minItems and maxItems set to 1 — exactly one URL, no more, no less. If you need multi-reference blending, look at a model whose schema documents a wider range, like seedream-5.0-lite.
Why do I get "additional properties 'resolution' not allowed"?
Input schemas are per-model on the task API. flux-2-klein-9b/image-to-image doesn't take a resolution or size field at all — output dimensions follow the input image and aspect_ratio. Drop any sizing fields carried over from another model's payload.
Can I send the reference image as base64 instead of a URL?
This model's schema takes image_urls — an array of URLs. Host your image at a reachable HTTPS URL (any CDN, object storage bucket, or presigned URL works) and pass that.
What does seed actually control?
Passing the same seed with the same prompt and reference image reproduces the same generation, which is useful for controlled prompt A/B tests. Omit it for normal traffic and let each call get its own random seed.
Does the same code work for flux-2-klein-9b text-to-image?
Same endpoint and flow, but text-to-image has its own input schema (no image_urls). Swap the model id to flux-2-klein-9b/text-to-image and drop the reference image — then verify with a test call, since required fields differ per mode.
What does it cost per image? Pricing is usage-based per generation. Check the live rate on the pricing page.



