HiAPI
Blog

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

Oct 31, 2026 10 min read
博客封面图:错位叠放的骨白色纸张,炭灰边缘,夹一张锈红色纸片,扁平投影。

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 to nano-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 — when true, the API returns a job handle immediately. Set it to false if 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 whether null means "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:

  1. Generate the first frame without consistency, iterating on the prompt until the character looks right.
  2. Use that output as your reference image.
  3. Turn on the consistency control and generate subsequent frames, keeping the reference fixed.
  4. 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 failed and cancelled as 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/json or text/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 Authorization header returns an auth error, not an image. Confirm the header reads Bearer <key> with a space.
  • Sending the wrong model value routes to the wrong place or fails outright. It should be exactly nano-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: false with 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 failed as 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 .png extension 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 →

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