HiAPI
Blog

Tutorial

How to Use imagen4-ultra via the HiAPI API: curl and Python Examples

A working request walkthrough for high-quality text-to-image on HiAPI

Oct 31, 2026 10 min read
博客封面图:淡蓝底上多层半透明玻璃平面叠错排列,折射出克制的冷蓝色带。

What imagen4-ultra on HiAPI gives you

imagen4-ultra is HiAPI's high-quality text-to-image model: you send a prompt, you get back a generated image. What makes it practical for developers is that it lives behind the same unified HiAPI interface as every other model on the platform, so the authentication, request shape, and job-handling patterns you learn here carry over to other endpoints.

This tutorial is a working walkthrough, not a parameter dump. By the end you will have:

  • a copy-pasteable curl request that submits a text-to-image job,
  • the same request rewritten as a small Python client,
  • a clear picture of how aspect ratio and other options affect output,
  • and a polling loop that waits for the job to finish and saves the image to disk.

A quick expectation-setting note: image generation is not instant. imagen4-ultra runs asynchronously — you submit a job, receive a job identifier, then poll until the render is complete. If you have used other async HiAPI endpoints, this flow will feel familiar; if you have not, the polling section covers the full lifecycle.

We will keep everything in HiAPI's own API surface: no vendor-specific SDKs, no third-party dashboards. One base URL, one key, one request format.

Before you send the first request

You need three things before the first call works.

1. An API key

Generate a key from your HiAPI account dashboard. Treat it like a password: store it in an environment variable rather than pasting it into source files.

export HIAPI_API_KEY="your-key-here"

2. The base URL

All HiAPI requests go to the same API host documented in the platform docs. Keep it in one place in your code so you never hardcode it in multiple spots:

export HIAPI_BASE_URL="https://api.hiapi.ai"

If your account or region routes through a different host, use the value shown in your dashboard — the request paths below stay the same.

3. How authentication is passed

HiAPI uses a bearer token in the Authorization header. Every request — submit and poll alike — carries the same two headers:

Authorization: Bearer $HIAPI_API_KEY
Content-Type: application/json

That is the whole auth story. There is no request signing step and no per-call token exchange. If you can set a header, you can call the API.

One sanity check before moving on: confirm your key is exported in the same shell session you will run curl from. A surprisingly large share of "401 Unauthorized" reports come down to a key that was exported in one terminal tab and used in another.

We will assume the model identifier imagen4-ultra throughout. Check the model listing in the docs if you want to confirm the exact string your account exposes.

The working curl request

Here is a complete submission request. It sends a prompt, sets the output aspect ratio, and asks for a single image.

curl -sS -X POST "$HIAPI_BASE_URL/v1/images/generations" \
  -H "Authorization: Bearer $HIAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "imagen4-ultra",
    "prompt": "A lone lighthouse on a rocky coast at dusk, long exposure, dramatic clouds, cinematic lighting",
    "aspect_ratio": "16:9",
    "n": 1
  }'

Walking through the payload field by field:

  • model — the identifier for the model you are calling. This is what routes the request; without it, the API cannot know which model to run.
  • prompt — the natural-language description of the image. This is the main lever on output quality. Be specific about subject, lighting, style, and composition; vague prompts produce vague images.
  • aspect_ratio — the shape of the output canvas, expressed as width:height. Covered in detail in a later section.
  • n — how many images to generate for this submission. Keep it at 1 while you are testing so each run is cheap to reason about.

Depending on the endpoint configuration, the response will either contain the finished image or — more commonly for this kind of model — a job object with an identifier and a status field. Capture that identifier; the polling section depends on it.

# typical shape of the submission response
{
  "id": "job_abc123",
  "status": "queued"
}

Pipe the response through a JSON formatter (| jq . if you have it) so you can read the field names against your own account's response without squinting at a single line.

The same request in Python

The Python version is a line-for-line translation — same URL, same headers, same body. Using requests keeps the mapping obvious:

import os
import requests

API_KEY = os.environ["HIAPI_API_KEY"]
BASE_URL = os.environ.get("HIAPI_BASE_URL", "https://api.hiapi.ai")

headers = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json",
}

payload = {
    "model": "imagen4-ultra",
    "prompt": "A lone lighthouse on a rocky coast at dusk, long exposure, dramatic clouds, cinematic lighting",
    "aspect_ratio": "16:9",
    "n": 1,
}

response = requests.post(
    f"{BASE_URL}/v1/images/generations",
    headers=headers,
    json=payload,
    timeout=30,
)
response.raise_for_status()
job = response.json()
print(job["id"], job["status"])

A few things worth keeping when you move this into a real service:

  • Read the key from the environment, never from a literal in the file. The os.environ[...] form fails loudly if the variable is missing, which is what you want.
  • Set an explicit timeout on the submit call. The job itself runs asynchronously, so the HTTP request should return quickly; a 30-second ceiling catches network problems instead of hanging.
  • Call raise_for_status() before touching the JSON. A 4xx response body is not the shape you expect, and failing early produces a clearer traceback than a KeyError three lines later.
  • Reuse a requests.Session if you are submitting many jobs — the connection pooling matters more than it looks.

If you prefer httpx or aiohttp, the translation is mechanical: same headers dict, same JSON body, same path. Nothing about the request depends on the HTTP library.

Aspect ratios and request options

Aspect ratio is the option you will touch most often, because it changes both composition and usable output size for downstream layouts.

Aspect ratio is passed as a "width:height" string in the request body — "1:1", "16:9", "9:16", "4:3", "3:2", and so on. Pick the ratio that matches where the image will live:

  • 1:1 — avatars, product tiles, social posts that crop to square.
  • 16:9 / 9:16 — hero banners, video thumbnails, story-format placements.
  • 4:3 / 3:2 — classic photography framing, gallery layouts.

Set the ratio deliberately rather than generating square and cropping later. Cropping throws away pixels you already paid to generate, and it can cut the subject out of frame. If you know the final placement, encode it in the request.

Beyond aspect ratio, the options that matter in day-to-day use:

  • prompt — still the highest-leverage input. Iterate here before you iterate anywhere else.
  • n — number of images per submission. Useful when you want variations to choose from; keep it low during prompt exploration.
  • model — the exact identifier for the model variant. If your account exposes multiple image models, this is the switch between them.

Stick to parameters documented for the endpoint rather than guessing at names. Unknown fields are a common cause of 400 responses, and a misspelled option is easy to miss in a large payload. When in doubt, send the minimal body — model, prompt, aspect_ratio — confirm it works, then add options one at a time.

Polling an async job and downloading the result

Image generation takes time, so the submit call returns a job, not an image. The lifecycle is: submit → poll status → download output.

Step 1: submit and capture the job ID

Reuse the request from earlier and pull id out of the response:

job = response.json()
job_id = job["id"]

Step 2: poll until the job reaches a terminal state

Poll a status endpoint with the same auth headers, sleeping between attempts. Back off progressively so you are not hammering the API:

import time

def wait_for_job(job_id, headers, base_url, timeout=300):
    deadline = time.time() + timeout
    delay = 2
    while time.time() < deadline:
        r = requests.get(
            f"{base_url}/v1/images/generations/{job_id}",
            headers=headers,
            timeout=30,
        )
        r.raise_for_status()
        data = r.json()
        status = data["status"]
        if status == "succeeded":
            return data
        if status == "failed":
            raise RuntimeError(f"job failed: {data}")
        time.sleep(delay)
        delay = min(delay * 2, 15)
    raise TimeoutError(f"job {job_id} not finished within {timeout}s")

The important details: a hard deadline, a cap on the backoff interval, and an explicit branch for the failed state so a broken job surfaces as an exception instead of a hang.

Step 3: download the image

The finished job response contains the output location — a URL or an equivalent artifact reference. Fetch it and write the bytes to disk:

result = wait_for_job(job_id, headers, BASE_URL)
image_url = result["output"][0]["url"]

img = requests.get(image_url, timeout=60)
img.raise_for_status()
with open("lighthouse.png", "wb") as f:
    f.write(img.content)

Two habits that prevent most production surprises: always write bytes ("wb"), never text mode; and treat the download URL as ephemeral — save the file to your own storage right away rather than relying on the link remaining valid indefinitely.

Common errors and how to fix them

Most failures fall into a handful of buckets. Here is what each looks like and how to resolve it.

401 Unauthorized

The key is missing, malformed, or not visible to the process making the request.

  • Check the header is exactly Authorization: Bearer <key> — the word Bearer, a space, then the key.
  • Confirm the environment variable is set in the same shell or process. In containers and CI, an exported variable in your local shell does not travel with the code.
  • If you recently rotated keys, make sure the old one is not still cached somewhere.

400 Bad Request

Usually a payload problem.

  • Unknown or misspelled field names — stick to the documented parameters.
  • An aspect_ratio value the endpoint does not accept — use the documented width:height forms.
  • n outside the allowed range.
  • Malformed JSON — a trailing comma or an unescaped quote inside the prompt string. Pretty-print the payload before sending if you suspect this.

404 on the status or download step

The job ID is wrong, or you are polling a path that does not match the endpoint you submitted to. Log the submit response in full the first time you integrate, and compare the ID character by character.

Polling timeouts

If wait_for_job hits its deadline:

  • Confirm you are checking the right status values; a terminal state you do not recognize will look like a hang.
  • Increase the overall timeout for large, complex renders rather than shortening the poll interval.
  • Log every status response during integration so you can see how long jobs actually take for your prompt mix.

Job status returns failed

Read the error body the API returns — it is there for a reason. Failed jobs are usually prompt-side or parameter-side, and the fix is in the request, not the polling code.

When something is unclear, the HiAPI docs for the model are the source of truth for field names and accepted values. Validate one change at a time, and keep a known-good request saved as your reference for the next integration.

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