HiAPI
Blog

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

Oct 31, 2026 11 min read
平面玻璃风格封面图:数层半透明玻璃斜向叠压,把浅灰底场折射成冷蓝色带,构图安静克制。

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:

  1. Synchronous-style payload — the response contains the generated image location (a URL) directly.
  2. Asynchronous job payload — the response contains a job identifier and a status (pending, processing, succeeded, failed are 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: Bearer header. 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 →

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