Tutorial
How to Use flux-2-klein-4b/text-to-image via the HiAPI API: curl, Python, and a Working Request
A hands-on tutorial for wiring the cost-efficient image tier into your pipeline
Why flux-2-klein-4b/text-to-image Fits Cost-Sensitive Workloads
Not every image pipeline needs the heaviest model in the catalog. A large share of production traffic is repetitive: catalog shots, variant previews, thumbnails for listings, social cards generated from the same template. For those, flux-2-klein-4b/text-to-image is designed as a deliberately lighter tier — you trade a bit of maximum fidelity for throughput and predictability, which is usually the right trade when you are generating thousands of assets on a schedule.
Two workloads benefit most:
- E-commerce product imagery. You often have a clean base description ("matte ceramic mug, 350ml, warm grey, on a white sweep") and need many rendered variants across colors, angles, and framing. Consistency matters more than artistic flourish.
- Bulk generation for internal or long-tail surfaces. Category banners, A/B test creatives, placeholder art for draft listings — assets where a marginal quality gain does not justify a heavier run.
Pick the heavier tier when you are producing hero images where fine texture and composition are the product. Pick flux-2-klein-4b/text-to-image when volume, latency, and cost per generated image dominate the decision. The rest of this tutorial uses that tier end to end: request shape, a curl call, a Python helper, aspect ratio strategy, and the async lifecycle.
Before You Send a Request
Three things need to be settled before your first call.
1. An API key from HiAPI
Create a key from your HiAPI dashboard. Every request described here is authenticated with that key, and the key is the only credential you need — there is no separate per-model signup.
2. Keep the key out of source control
Treat the key like any other secret. A pattern that works well:
export HIAPI_API_KEY="your-key-here"
Then read it from the environment in both shell and Python. Never commit it, and add .env to .gitignore if you use a dotfile loader. If a key leaks, rotate it in the dashboard rather than trying to scrub history.
3. Confirm the model identifier
The identifier string is used verbatim in the request body and in the URL in some setups:
flux-2-klein-4b/text-to-image
Copy it exactly — the 4b and the /text-to-image suffix are part of the name. Typos here are the single most common cause of a 404 or an "unknown model" error. Once you have a key in the environment and the identifier confirmed, you are ready to build the payload.
Anatomy of a flux-2-klein-4b/text-to-image Request
A text-to-image request on HiAPI is small by design. You supply a model identifier and a payload of generation parameters; you get back either a result or a job handle, depending on how the endpoint is configured for that model.
The fields that matter first
model— the exact identifier,flux-2-klein-4b/text-to-image.prompt— a single string describing the image. This is the field with the largest effect on output quality; spend your effort here before touching anything else.- Dimensions or aspect ratio — how you specify the framing of the output. Prefer the aspect-ratio style of specification over hardcoded pixel pairs, because it stays stable if the underlying resolution behavior changes.
What a response looks like
Responses fall into two shapes, and your client should handle both:
- Synchronous-style payload — the response contains the generated image location (a URL) directly.
- Asynchronous job payload — the response contains a job identifier and a status (
pending,processing,succeeded,failedare the typical states). You then poll a status endpoint with that identifier until it reaches a terminal state and the payload carries the output location.
Do not invent parameters you cannot verify. If a field is not part of the documented request for this model, leave it out — unknown fields are the second-most-common source of confusing 400s. Start with model, prompt, and the aspect ratio, get a working image, then add optional parameters one at a time and check the output after each change.
Your First Working Call with curl
Here is a complete request you can paste into a terminal. It assumes HIAPI_API_KEY is exported as described above.
curl -sS -X POST "https://api.hiapi.ai/v1/images/generations" \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "flux-2-klein-4b/text-to-image",
"prompt": "Studio product photo of a matte ceramic mug, warm grey, soft diffused light, seamless white background, centered composition",
"aspect_ratio": "1:1"
}'
Pipe it through a JSON formatter (| jq .) so you can read the response. You will see either an image URL directly or a job object with an id and a status.
Reading the result
- If you get an image URL, open it in a browser. That confirms the key, the model identifier, and the prompt all work.
- If you get a job object, note the
id— you will poll it in a later section.
Diagnosing the common failures
- 401 / 403 — the key is missing, malformed, or not passed in the
Authorization: Bearerheader. Check the environment variable is actually exported in the shell running curl. - 404 or "unknown model" — the identifier is wrong. It must be exactly
flux-2-klein-4b/text-to-image. - 400 with a validation message — usually a missing
prompt, an unsupported parameter name, or an aspect ratio that is not in the accepted set. Trim the payload back to the three core fields and retry. - 429 — you are sending requests faster than your tier allows. Add backoff rather than retrying in a tight loop.
Once curl works, you have a known-good request to compare against when the Python version misbehaves.
Porting the Same Request to Python
The Python version is a thin wrapper around the same payload. Structure it as a function so the rest of your pipeline can call it without duplicating HTTP logic.
import os
import time
import requests
API_BASE = "https://api.hiapi.ai/v1"
MODEL = "flux-2-klein-4b/text-to-image"
def generate_image(prompt: str, aspect_ratio: str = "1:1") -> dict:
"""Submit a text-to-image job and return the parsed response."""
api_key = os.environ["HIAPI_API_KEY"]
payload = {
"model": MODEL,
"prompt": prompt,
"aspect_ratio": aspect_ratio,
}
response = requests.post(
f"{API_BASE}/images/generations",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
json=payload,
timeout=60,
)
if response.status_code >= 400:
# Surface the server's message rather than a bare status code.
raise RuntimeError(f"HiAPI error {response.status_code}: {response.text}")
return response.json()
A few habits that pay off immediately:
- Read the key from the environment inside the function, not at import time, so the same module works in a notebook, a script, and a scheduled job.
- Check the status code explicitly and include the response body in the exception. HiAPI validation messages tell you which field is wrong; a bare
raise_for_status()hides that. - Set a timeout. Without one, a stalled connection can hang a worker indefinitely.
- Keep the payload as a dict so you can log it, diff it, and extend it with optional parameters without rewriting string formatting.
Now a minimal driver:
result = generate_image(
"Studio product photo of a matte ceramic mug, warm grey, "
"soft diffused light, seamless white background, centered composition"
)
print(result)
Choosing Aspect Ratios That Survive Production
Aspect ratio is the parameter most likely to cause rework later, because downstream systems assume framing. Decide it per surface, not per request.
A practical mapping
- Square (1:1) — marketplace listings, catalog grids, avatar and thumbnail slots. This is the safe default for e-commerce because most product tiles are square and crop poorly.
- Portrait (3:4 or 4:5) — social feed placements and lookbook-style product shots. Gives vertical products like bottles and apparel room without letterboxing.
- Landscape (16:9) — banners, hero strips, email headers, video thumbnails.
Why to lock the ratio upstream
If the ratio is chosen at render time by whatever code happens to call the API, you get a catalog where half the tiles are square and half are portrait, and you pay for a re-generation pass to fix it. Instead, encode the ratio in your content model — a surface or placement field that maps deterministically to a ratio — and generate from that. Crop-tolerant slots get 1:1; slots with a fixed layout get the ratio the layout demands.
The same discipline applies to the prompt: include framing hints ("centered composition", "full product in frame", "negative space on the left for copy") in the prompt template rather than in ad-hoc per-call strings. Templates keep a thousand generated images looking like they came from one pipeline, which is exactly what a product catalog needs.
One caveat: not every ratio is accepted for every model. Send your chosen ratio once, confirm it succeeds, and only then roll it out across the batch.
Polling an Async Job to Completion
When flux-2-klein-4b/text-to-image returns a job handle instead of an image, treat it as a state machine. The flow is: submit → receive job id → poll status → terminal state → read output.
def wait_for_job(job_id: str, timeout_s: int = 300, interval_s: int = 2) -> dict:
api_key = os.environ["HIAPI_API_KEY"]
deadline = time.time() + timeout_s
while time.time() < deadline:
response = requests.get(
f"{API_BASE}/jobs/{job_id}",
headers={"Authorization": f"Bearer {api_key}"},
timeout=30,
)
response.raise_for_status()
job = response.json()
status = job.get("status")
if status == "succeeded":
return job
if status == "failed":
raise RuntimeError(f"Job {job_id} failed: {job.get('error')}")
time.sleep(interval_s)
raise TimeoutError(f"Job {job_id} did not finish in {timeout_s}s")
Handling each state
pending/processing— keep waiting. Start with a short interval and consider a mild backoff (for example, grow the sleep toward a ceiling) so you do not hammer the status endpoint during long queues.succeeded— the payload now contains the output location. Stop polling immediately.failed— do not retry blindly. Log the error message; if it is a prompt or parameter problem, resubmitting the identical request will fail identically.
Knowing when to stop
Always bound the loop with a wall-clock deadline. A worker that polls forever is a worker that leaks. On timeout, surface the job id so it can be checked manually — and if the job later succeeds, you can still retrieve the output. For batches, run several jobs concurrently and poll them together rather than serializing submit-and-wait for each image.
Downloading and Storing the Output
The generated image arrives as a retrievable location in the job or generation response. Fetch it server-side, then persist it yourself — do not hotlink the returned URL into a product page, since it is a delivery artifact, not your storage layer.
def download_image(url: str, path: str) -> str:
response = requests.get(url, timeout=120)
response.raise_for_status()
with open(path, "wb") as f:
f.write(response.content)
return path
Use a predictable naming scheme
A path like images/{surface}/{sku}/{variant}-{ratio}.png gives you three things: assets group naturally by product, the surface tells you which framing was requested, and the ratio in the filename makes accidental duplicates obvious. Include a short hash of the prompt in the filename if you regenerate variants of the same SKU — that way a re-run does not silently overwrite a previous attempt you might want to compare against.
Close the loop
Record the job id alongside the stored file, in a manifest, database row, or sidecar file. When a downstream system reports a bad image, the job id is what lets you pull the original prompt and parameters back out. Combined with the content-addressed filename, that gives you a reproducible audit trail from stored asset back to the exact request that produced it.
With submission, polling, and download wired together, flux-2-klein-4b/text-to-image becomes an ordinary step in your pipeline: queue the work, poll with a deadline, store with a stable name. From there, scaling up is a matter of concurrency and templates, not new code.
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