Tutorial
How to Call the Imagen 4 API via HiAPI: curl and Python Examples
What the Imagen 4 endpoint gives you
If you already have a prompt and want an image back without wiring up a billing account, a queue, or a storage bucket on a provider you have never used before, the Google-Imagen4 model on HiAPI is a reasonable place to start. You send a text prompt to a single HTTP endpoint, authenticate with one API key, and get back a reference to the generated image (or a job you can poll). Everything else — model hosting, capacity, retries on the provider side — is abstracted away.
A text-to-image call through this endpoint covers three things you will care about in practice:
- Prompt to image. The prompt is the main input. The model returns one or more images depending on how you configure the request.
- Output shape control. You can request an aspect ratio (square, landscape, portrait) at request time rather than cropping afterwards. This matters more than it sounds: cropping a generated image loses composition the model already planned for.
- Asynchronous execution. Longer generations are handled as jobs. You submit, poll for status, then fetch the result. Short calls may return quickly, but you should write your client as if every call is asynchronous, because that is the flow that survives production traffic.
The rest of this tutorial builds one request three ways: a raw curl call you can paste into a terminal, an equivalent Python client, and then the pieces you need for aspect ratios, polling, persistence, and error handling. If you want to generate an image with a prompt in one line of shell, this fits. If you need fine-grained control over inpainting masks or a training loop, it probably does not.
Two assumptions before you start: you have a HiAPI API key, and you can make outbound HTTPS requests from wherever your code runs. Nothing else is required — no SDK install is mandatory, though the Python examples below use requests.
Your first request with curl
The fastest way to confirm your key works is a single curl call. The endpoint takes a JSON body, so you need two headers: Content-Type for the body and Authorization for the key. Read the key from an environment variable rather than pasting it into the command, so it does not end up in your shell history.
export HIAPI_API_KEY="your-key-here"
curl -sS -X POST "https://api.hiapi.ai/v1/images/generations" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-d '{
"model": "google-imagen-4",
"prompt": "a weathered lighthouse on a basalt cliff, morning fog, wide shot",
"n": 1,
"aspect_ratio": "16:9"
}'
A few things to notice in that body:
modelselects Google-Imagen4. Keep it explicit even if the service has a default — it makes your code readable a year later.promptis the only required creative input. Write it the way you would describe the shot to a photographer: subject, setting, light, framing.ncontrols how many images you ask for. Start with one while you are getting the response shape right; raise it once you trust your pipeline.aspect_ratiois covered in detail later. Omitting it falls back to the service default.
A successful response returns a JSON object. The part you care about is the list of image entries; each entry carries a reference you can download or a URL you can hand to a frontend. If you request multiple images, expect multiple entries in the same order as your request. If the generation is long enough to be queued, the response instead gives you an identifier and a status you can poll — that flow is described in its own section below.
If you get a 4xx, check the two headers first: a missing Bearer prefix or a key with trailing whitespace accounts for most first-call failures. Use -i to see the status line and error body when debugging.
The same request in Python
In a script, wrap the call in a small function so the rest of your code never touches raw HTTP. The function should take a prompt and optional parameters, build the body, send it, and return the parsed JSON.
import os
import requests
HIAPI_BASE = "https://api.hiapi.ai/v1"
def generate_image(prompt: str, aspect_ratio: str = "1:1", n: int = 1) -> dict:
response = requests.post(
f"{HIAPI_BASE}/images/generations",
headers={
"Authorization": f"Bearer {os.environ['HIAPI_API_KEY']}",
"Content-Type": "application/json",
},
json={
"model": "google-imagen-4",
"prompt": prompt,
"n": n,
"aspect_ratio": aspect_ratio,
},
timeout=120,
)
response.raise_for_status()
return response.json()
if __name__ == "__main__":
result = generate_image(
"a weathered lighthouse on a basalt cliff, morning fog, wide shot",
aspect_ratio="16:9",
)
print(result)
Three design choices in that snippet are worth keeping when you adapt it:
An explicit timeout. requests has no default timeout, which means a stalled connection can hang a worker indefinitely. Pick a number above your slowest expected generation and let it fail loudly.
raise_for_status() before parsing. Error responses do not always share the success schema. Check the HTTP status first, then touch the body.
Small, parameterized function. Once you add polling and downloads, this function stays the single entry point for submission, and the lifecycle logic lives next to it rather than tangled inside it.
The parsed result contains the same information as the curl output. For a synchronous response, iterate the image entries and pull out the URL. For an asynchronous response, pull out the job identifier and pass it to your polling function. Your code should branch on which shape it receives rather than assuming one.
Controlling aspect ratio and output
Aspect ratio is the parameter you will adjust most often, and it is worth setting properly rather than cropping later. If you ask for a square image and then crop a 16:9 band out of it, you throw away the top and bottom of a composition the model deliberately framed. Asking for 16:9 up front lets the model plan the shot for that frame.
Pass it in the request body alongside the prompt:
result = generate_image(
"a sprawling night market, neon signs, steam from food stalls",
aspect_ratio="16:9",
n=2,
)
Use a landscape ratio for hero banners, thumbnails for wide cards, and product photography; a portrait ratio for mobile-first cards, posters, and character shots; and a square ratio for avatars and social posts where the crop is unpredictable. Decide based on where the image will be placed, not on what looks fine in isolation.
What changes in the response when you change the ratio: the pixel dimensions of the returned images. The number of entries, their order, and the download flow stay the same, so your downstream code does not need to branch on ratio. If your UI reserves space for an image before it loads, read the dimensions from the response and set them on the element rather than hardcoding a size.
Two other output knobs are worth knowing:
- Image count (
n). More images per request means more choices, but also more tokens spent on a prompt that may be wrong. While iterating on prompt wording, request one image; batch only once the prompt is stable. - Prompt wording for framing. Ratio sets the frame; the prompt sets the composition inside it. Words like "wide shot," "close-up," and "centered" do real work alongside the ratio parameter.
Billing for this endpoint follows the image generation itself — check the price panel on the site for the current rates rather than hardcoding anything into your cost model.
Handling asynchronous jobs and polling
Where a generation takes long enough to be queued, the endpoint returns before the image exists. You get back a job handle, and your job is to check it until it reaches a terminal state. Write this flow even if today's calls return quickly — capacity, prompt complexity, and image count all affect latency, and a client built for the synchronous case breaks the first time a request is slow.
The lifecycle has three steps:
- Submit. Send the generation request as usual. If the response is a job, extract its identifier.
- Poll. Call the job-status endpoint with that identifier on a fixed interval.
- Fetch. Once status reports success, read the image references from the result payload.
import time
def wait_for_job(job_id: str, interval: float = 2.0, max_attempts: int = 60) -> dict:
for attempt in range(max_attempts):
response = requests.get(
f"{HIAPI_BASE}/images/jobs/{job_id}",
headers={"Authorization": f"Bearer {os.environ['HIAPI_API_KEY']}"},
timeout=30,
)
response.raise_for_status()
payload = response.json()
status = payload.get("status")
if status == "succeeded":
return payload
if status in {"failed", "cancelled"}:
raise RuntimeError(f"job {job_id} ended with status {status}")
time.sleep(interval)
raise TimeoutError(f"job {job_id} did not finish in time")
The details that make this production-safe:
- Bounded attempts, not an infinite loop. A job that never completes should surface as a timeout error you can alert on, not a worker that hangs forever.
max_attemptstimesintervalis your wall-clock budget — size it for your slowest realistic generation. - Backoff under load. A fixed two-second interval is fine for interactive use. For batch workloads, back off gradually or add jitter so retries from many workers do not synchronize on the same second.
- Distinguish failure from unknown. Only
failedandcancelledare terminal failures. Treat any other non-success status as "still working" and poll again; adding a branch for an unrecognized status that raises immediately will break when the service adds a new state. - Retry submission, not polling, on network errors. If the submit call itself times out, a retry may produce a duplicate job. Give each submission an idempotency key if the API supports one, or deduplicate on your side by prompt hash plus timestamp.
Once the job reports success, stop polling. Persisting the result and keeping the job handle for audit is cheaper than continuing to ask for a status you already have.
Downloading and storing the images
The generation response gives you references, not image bytes. In most cases each image entry carries a URL you can fetch with a plain GET. Treat that URL as temporary: download promptly, verify it, and store the bytes in whatever you actually control.
import pathlib
import uuid
def download_images(payload: dict, out_dir: str = "images") -> list[pathlib.Path]:
target = pathlib.Path(out_dir)
target.mkdir(parents=True, exist_ok=True)
saved = []
for entry in payload.get("data", []):
url = entry["url"]
resp = requests.get(url, timeout=60)
resp.raise_for_status()
content_type = resp.headers.get("Content-Type", "")
if not content_type.startswith("image/"):
raise ValueError(f"unexpected content type: {content_type}")
path = target / f"{uuid.uuid4().hex}.png"
path.write_bytes(resp.content)
saved.append(path)
return saved
A few checks before you treat a job as done:
- Content type and size. A redirect to an error page returns 200 with HTML in some setups. Verify
Content-Typestarts withimage/and that the body is non-trivial in size before writing to your permanent store. - Naming. Generate a fresh name per download rather than deriving it from the prompt. Prompts repeat, collide, and contain characters your filesystem rejects.
- Store the prompt next to the file. Save the prompt, parameters, and job identifier in your own database or a sidecar metadata file. Six months from now, the image alone will not tell you which request produced it.
- Move, do not copy, into permanent storage. Write to a temp location, verify, then move to your bucket or CDN-backed store. A half-written file in the permanent store is worse than a failed job.
If your pipeline produces many images, keep the download step separate from the generation step. Generation is the expensive, rate-limited part; downloads are cheap to retry and should never cause you to regenerate.
Common errors and debugging checklist
Most first-day failures fall into a small set of buckets. Work through them in this order — each check is faster than the one after it.
Auth failures (401, 403)
The key is wrong, missing, or malformed. Check that the header value is exactly Bearer plus the key, with no line breaks from a wrapped paste, and that the environment variable is actually exported in the shell or process running the code. A key rotated in the dashboard invalidates the old one immediately — if another service just started failing, ask whether someone rotated keys.
Parameter rejections (400, 422)
The body does not match the schema. The usual culprits: a misspelled model identifier, an aspect_ratio string in a format the endpoint does not accept, or n outside the supported range. Print the error body — it usually names the offending field. Compare your JSON against the working curl example and diff it field by field.
Timeouts
Two different timeouts get confused. A client-side timeout means your HTTP call gave up; the job may still be running. A job that never reaches a terminal state means polling budget is too small or the request legitimately failed. Set your client timeout above your slowest expected generation, and set your polling budget above that again. When a client timeout fires during submission, retrying blindly can duplicate work — check for an existing job before resubmitting.
Downloads that look successful but are not
A 200 status with an HTML body, or a zero-byte file, usually means the image URL expired or was fetched without the right access. Download immediately after the job succeeds, and validate content type and size as shown above.
Rate limiting (429)
Back off and retry rather than tightening the loop. Exponential backoff with jitter is the standard answer; cap the number of retries and surface persistent failures to your monitoring.
A debugging habit that pays for itself
Log four things for every request: the model identifier, the full request body with secrets stripped, the job identifier (or response status for synchronous calls), and the elapsed time. When something breaks in production, those four fields usually identify whether the problem is your client, your parameters, or the queue — in under a minute.
Start with the curl call, get one image back, then move the logic into the Python client and add polling and persistence around it. That path keeps each failure small and attributable.
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