Tutorial
How to Use nano-banana via the HiAPI API: curl, Python, and a Working Request
A hands-on walkthrough from first request to downloaded output
What a nano-banana Request Looks Like
Before writing code, it helps to know the lifecycle of a nano-banana call on HiAPI. Every request follows the same pattern: you send a JSON body describing what you want, HiAPI accepts the job, and you either get a completed result back or a job handle you use to poll for completion.
A few characteristics define the shape:
- One endpoint, one API key. All nano-banana traffic goes through the HiAPI unified interface. You authenticate with your HiAPI key and never touch anything upstream.
- JSON in, JSON out. The request body is JSON. The response is JSON too, with either an inline result or a job id plus a status field.
- Text is required; images are optional. At minimum you send a prompt. You can add reference images to steer subject, style, or characters.
- Async-friendly. Image generation can take longer than a typical HTTP timeout, so the API supports asynchronous jobs you poll until they finish.
The rough anatomy of a request body:
{
"model": "nano-banana",
"prompt": "a description of the image you want",
"image_input": ["https://.../reference.jpg"],
"consistency": "character",
"async": true
}
And the response, when asynchronous:
{
"id": "job_abc123",
"status": "queued",
"created_at": "..."
}
When the job completes and you fetch it, status becomes succeeded (or failed) and a result payload carries the output URL. Keep this two-phase model in mind, because everything in the rest of this walkthrough builds on it.
A Working curl Request
Here is the smallest request that produces an image. It assumes HIAPI_API_KEY is exported in your shell:
curl -X POST https://api.hiapi.ai/v1/images/generations \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "nano-banana",
"prompt": "a cozy reading nook by a rain-streaked window, warm lamp light, watercolor style",
"async": true
}'
Field by field:
model— set tonano-banana. This is how HiAPI routes the request.prompt— the only strictly required creative field. Be specific about subject, composition, and style; a vague prompt is the most common cause of disappointing output.async— whentrue, the API returns a job handle immediately. Set it tofalseif you want to block until completion, but be prepared for longer response times.image_input— optional array of reference image URLs. Omit it for pure text-to-image.consistency— optional control for characters or subjects across images; more on that below.
If you are unsure which optional parameters the current version supports, check the model's documentation page on HiAPI before adding fields — unknown fields may be ignored or rejected depending on the parameter.
Run the command and inspect the JSON. Assuming async is true, you will get back a job object rather than an image. Save the id; that is your handle for the next step.
Porting the Request to Python
The curl call translates directly. The goal here is not just a one-off script but a reusable wrapper you can drop into a pipeline.
import os
import time
import requests
BASE = "https://api.hiapi.ai"
HEADERS = {
"Authorization": f"Bearer {os.environ['HIAPI_API_KEY']}",
"Content-Type": "application/json",
}
def create_image(prompt, image_input=None, consistency=None, async_job=True):
body = {"model": "nano-banana", "prompt": prompt, "async": async_job}
if image_input:
body["image_input"] = image_input
if consistency:
body["consistency"] = consistency
resp = requests.post(
f"{BASE}/v1/images/generations",
headers=HEADERS,
json=body,
timeout=60,
)
if resp.status_code >= 400:
raise RuntimeError(f"create failed: {resp.status_code} {resp.text}")
return resp.json()
A few things that make this robust rather than merely functional:
- Explicit timeout. Always pass one; the default of "wait forever" turns a transient network issue into a hung process.
- Status checking before parsing. Raise on error responses so a bad request does not silently return a JSON body you then index into.
- Optional fields omitted, not nulled. Rather than sending
"image_input": null, only add the key when you have values. This keeps logs readable and avoids ambiguity about whethernullmeans "none" or "unset".
Wrapping it in a function also makes retries trivial: because the function raises on failure, a caller can wrap it in a retry loop without touching the request code. That separation pays off as soon as you move from one image to a batch.
Sending Text and Image Inputs
Text-to-image is the easy case. The interesting part of nano-banana is combining a prompt with reference images in a single request. You pass image_input as an array of URLs, and the prompt describes what to do with them.
result = create_image(
prompt="the person in the first image standing in a Tokyo alley at night, neon reflections, cinematic",
image_input=["https://example.com/portrait.jpg"],
)
Practical guidance for mixing modalities:
- Be explicit about roles. If you send more than one image, say in the prompt which one provides the subject, which provides the style, and which provides the background. "Combine these images" is ambiguous; "use the face from image one and the lighting from image two" is not.
- Use reachable, stable URLs. The API must be able to fetch the image. Signed URLs that expire in minutes are fine for a live call but will break if you retry a queued job later.
- Prefer clean references. A sharp, well-lit portrait produces a more faithful likeness than a busy group photo. Garbage in, garbage out applies more strongly to image conditioning than to text.
- Iterate on the prompt, not the references, first. If likeness is off, adjust how you describe the subject before swapping source images — it is usually the cheaper change.
When the reference contains a person you intend to reuse across multiple generations, you are now in character consistency territory, which is the next section.
Character Consistency Options
Character consistency answers a specific need: generating several images that plausibly feature the same character or subject. nano-banana exposes this as an optional control rather than a separate model.
At a high level, the tradeoffs look like this:
- No consistency control. Best for one-off images. Fast and simple, but if you regenerate, the character may drift. Use it for backgrounds, objects, and standalone illustrations.
- Reference-based consistency. You supply one or more images of the character and set the consistency option so the model anchors to them. This is the right choice for storyboards, comics, and product sequences where identity must hold across frames. The cost is that you must have a good reference image to begin with.
- Text-described consistency. You describe the character in detail in every prompt — hair color, clothing, build, distinctive features — without a reference. Useful when you have no source photo, but it demands discipline: keep the description in a reusable constant so it does not drift between calls.
A pragmatic workflow for a series:
- Generate the first frame without consistency, iterating on the prompt until the character looks right.
- Use that output as your reference image.
- Turn on the consistency control and generate subsequent frames, keeping the reference fixed.
- If the character still drifts, shorten the prompt and let the reference do more of the work — long prompts that contradict the reference confuse the model.
Consistency is a knob, not a guarantee. Expect to review output and regenerate a small fraction of frames even in a well-tuned pipeline.
Polling Asynchronous Jobs
When you submit with async: true, you get a job id back. Polling is straightforward, but the details matter more than they seem.
A minimal, correct poller:
def wait_for_job(job_id, timeout=300, initial_delay=2, max_delay=15):
deadline = time.time() + timeout
delay = initial_delay
while time.time() < deadline:
resp = requests.get(
f"{BASE}/v1/images/generations/{job_id}",
headers=HEADERS,
timeout=30,
)
resp.raise_for_status()
data = resp.json()
status = data.get("status")
if status == "succeeded":
return data
if status in ("failed", "cancelled"):
raise RuntimeError(f"job {job_id} ended as {status}: {data}")
time.sleep(delay)
delay = min(delay * 1.5, max_delay)
raise TimeoutError(f"job {job_id} did not finish within {timeout}s")
Why each choice:
- Exponential-ish backoff with a ceiling. Start at a couple of seconds and multiply by 1.5 up to a cap. A fixed one-second poll for a thirty-second job wastes dozens of requests; a fixed thirty-second poll adds needless latency to fast jobs.
- A hard deadline. Without one, a stuck job blocks your worker indefinitely. Fail loudly instead.
- Terminal-state handling. Treat
failedandcancelledas exceptions, not as states to keep polling. A job in a failure state will never succeed on the next poll. - A fresh GET per iteration. Do not cache the first response and assume it updates.
For batch work, submit all jobs first, collect the ids, then poll them in a loop — that overlaps generation time across jobs instead of serializing them.
Downloading and Verifying the Output
Once the job succeeds, the result payload contains an output URL. Downloading is a plain GET, but you should not assume the bytes are an image until you check them.
def download_image(url, path):
with requests.get(url, stream=True, timeout=60) as r:
r.raise_for_status()
content_type = r.headers.get("Content-Type", "")
if not content_type.startswith("image/"):
raise RuntimeError(f"unexpected content type: {content_type}")
with open(path, "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
return path
Sanity checks worth doing on every download:
- Content-Type. If you asked for an image and got
application/jsonortext/html, you actually received an error page. Checking the header catches this before you write a corrupt file. - Streaming. Stream to disk rather than loading the whole body into memory; it keeps memory flat when you download in parallel.
- Non-zero size. A zero-byte file means something went wrong even if the HTTP status was fine.
- Checksum or byte count in your logs. Record it alongside the job id so you can trace a bad file back to a specific generation.
Output URLs may be temporary. If you intend to keep results, download them into your own storage during the same run rather than saving links for later.
Common Failure Points
Most problems fall into a handful of buckets. Check these first before assuming the model is misbehaving.
Authentication and routing
- A missing or malformed
Authorizationheader returns an auth error, not an image. Confirm the header readsBearer <key>with a space. - Sending the wrong
modelvalue routes to the wrong place or fails outright. It should be exactlynano-banana.
Request body issues
- Vague prompts. The single most common cause of "bad output." Add composition, lighting, and style detail before changing anything else.
- Unreachable image URLs. A reference URL that 404s, requires auth, or points at a local path will fail or be silently ignored. Fetch the URL yourself once to confirm it works.
- Sending
async: falsewith a long prompt and hitting a client timeout. Switch to async and poll instead of raising the timeout.
Job handling
- Polling a job that never existed. A typo in the job id yields a not-found error that looks like a failure; log the id you submitted.
- Treating
failedas still-running. Always exit the poll loop on terminal states. - No deadline. The most costly bug in a batch job: one stuck image stalls the entire run.
Output handling
- Not checking Content-Type. As covered above, an error page saved with a
.pngextension looks like a corrupt image later. - Relying on a stale result URL hours after generation. Download promptly.
Work through authentication, then the body, then the job lifecycle, then the download — in that order — and most failures resolve in a few minutes.
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