
FLUX.2 [klein] 4B is Black Forest Labs' compact, fast image-editing model: give it one reference image and a natural-language instruction, and it returns an edited image at up to 4 MP. On hiapi it runs through the same Unified Async API as every other model — POST /v1/tasks to create a job, then poll or get a callback for the result. This guide covers the exact request shape, a working curl/Python example, and how to wire it into production.
sk-... and go in the Authorization: Bearer header.flux-2-klein-4b/image-to-image is resolution-tiered (0.25 MP through 4 MP) — see hiapi pricing for the live numbers rather than hardcoding them here.The model id is flux-2-klein-4b/image-to-image — use it exactly as written, with the slash, as the model value (not as part of the URL path). Every hiapi model shares one endpoint; only the input object's fields differ per model.
curl -X POST "https://api.hiapi.ai/v1/tasks" \
-H "Authorization: Bearer sk-YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "flux-2-klein-4b/image-to-image",
"input": {
"prompt": "Keep the subject and camera angle. Replace the background with a deep-blue studio gradient and add soft rim lighting.",
"image_urls": ["https://your-cdn.example.com/product-shot.jpg"],
"resolution": "2 MP",
"aspect_ratio": "16:9"
}
}'
This returns immediately with a taskId — generation happens asynchronously:
{"code": 0, "message": "success", "data": {"taskId": "task_..."}}
Poll for the result:
curl "https://api.hiapi.ai/v1/tasks/task_..." \
-H "Authorization: Bearer sk-YOUR_API_KEY"
When status reaches "success", the edited image is at data.output[0].url. That URL is temporary (it carries an expireAt), so download or re-host the bytes right away — don't store the hot link.
The same flow in Python:
import time
import requests
API = "https://api.hiapi.ai/v1/tasks"
HEADERS = {
"Authorization": "Bearer sk-YOUR_API_KEY",
"Content-Type": "application/json",
}
def create_task(prompt: str, image_url: str) -> str:
payload = {
"model": "flux-2-klein-4b/image-to-image",
"input": {
"prompt": prompt,
"image_urls": [image_url],
"resolution": "2 MP",
"aspect_ratio": "16:9",
},
}
resp = requests.post(API, headers=HEADERS, json=payload, timeout=30)
resp.raise_for_status()
return resp.json()["data"]["taskId"]
def wait_task(task_id: str, timeout_s: int = 300, poll_s: int = 5) -> dict:
deadline = time.time() + timeout_s
while time.time() < deadline:
r = requests.get(f"{API}/{task_id}", headers=HEADERS, timeout=30)
task = r.json()["data"]
if task["status"] == "success":
return task
if task["status"] == "fail":
raise RuntimeError(f"task {task_id} failed: {task.get('error')}")
time.sleep(poll_s)
raise TimeoutError(f"task {task_id} did not finish in {timeout_s}s")
task_id = create_task(
"Keep the subject and camera angle. Replace the background with a deep-blue "
"studio gradient and add soft rim lighting.",
"https://your-cdn.example.com/product-shot.jpg",
)
result = wait_task(task_id)
print(result["output"][0]["url"])
flux-2-klein-4b/image-to-image accepts a strict input schema — extra fields not listed here are rejected with a 400:
| Field | Required | Notes |
|---|---|---|
prompt | yes | Natural-language description of the edit to apply. |
image_urls | yes | Array with exactly one reference image URL. Passing more than one returns maxItems: got N, want 1. |
resolution | no | One of 0.25 MP, 0.5 MP, 1 MP (default), 2 MP, 4 MP. Draft cheap at 0.25/0.5 MP, finish at 2/4 MP. |
aspect_ratio | no | One of match_input_image (default), 1:1, 16:9, 9:16, 3:2, 2:3, 4:3, 3:4, 5:4, 4:5, 21:9, 9:21. |
seed | no | Integer, for reproducible output across otherwise-identical requests. |
output_format | no | jpg (default), png, or webp. |
output_quality | no | Integer, compression quality for jpg/webp (default 95). |
There's no size or n field on this model — those are conventions from other providers that this schema doesn't recognize. Every hiapi model has its own field set, so check the model reference page before porting a request built for a different model.
Prefer callbacks over polling. Add a top-level callback object and hiapi notifies your service instead of you polling in a loop:
{
"model": "flux-2-klein-4b/image-to-image",
"input": { "prompt": "...", "image_urls": ["..."] },
"callback": { "url": "https://your-domain.com/hiapi/callback", "when": "final" }
}
when only accepts "final" — one notification when the task reaches a terminal state (success or fail), not a progress stream. Treat the callback as a wake-up signal: on receipt, still GET /v1/tasks/<taskId> to read the authoritative status, and make your handler idempotent on taskId in case of a duplicate delivery.
Idempotency. Persist taskId right after create_task returns, before you start polling or waiting on a callback. If your process crashes mid-wait, resume with GET /v1/tasks/<taskId> on restart instead of re-creating the task — task creation is the billable event, GET is free to repeat.
Error handling. An invalid or unauthorized key returns HTTP 401 with a structured body:
{
"error": {
"code": "permission_denied",
"message": "This API key cannot use the selected model. Please check permissions or use another key. If the issue persists, contact support with request ID: ...",
"request_id": "...",
"type": "hiapi_error"
}
}
Log the request_id — support can look up the exact call from it. A malformed input (missing prompt/image_urls, an unsupported aspect_ratio value, or an unrecognized extra field) returns a 400 with error_code: "INVALID_REQUEST" and a message naming the offending field; fix the payload rather than retrying it unchanged.
POST /v1/tasks / GET /v1/tasks/:id contract shared by every model on the platform.What image formats does image_urls accept?
jpeg, png, or webp. svg is not supported. Images larger than 4 MP are automatically resized down to 4 MP before editing.
Can I pass more than one reference image?
No — image_urls on this model is capped at exactly one item. Passing more returns a 400 (maxItems: got N, want 1).
Why did my request get rejected with "additional properties ... not allowed"?
The schema is strict: any field not in the parameter table above (like size, n, or strength from other providers' conventions) triggers a 400. Check the model reference for the exact accepted field set.
How do I keep the original image's aspect ratio?
Set aspect_ratio to match_input_image — that's also the default if you omit the field entirely.
What's the difference between flux-2-klein-4b/image-to-image and the 9B variant?
Both share the same request schema (prompt, image_urls, resolution, aspect_ratio, seed, output_format, output_quality). The 9B route trades some speed and cost for stronger realism, text rendering, and detail — reach for 4B when you're iterating fast and cheap, 9B when the edit needs to hold up in fine detail.
Do I need a callback URL, or is polling fine?
Polling with GET /v1/tasks/:id is fine for local development, low-volume jobs, or as a fallback if a callback delivery is ever missed. For production traffic, a callback avoids the overhead of a polling loop per request.
Key Takeaways