HiAPI
Blog

Tutorial

How to Use nano-banana-pro via the HiAPI API: curl and Python Examples

Oct 31, 2026 12 min read
平面流体形态层叠交错的封面图,墨色底上流动着青瓷绿与石板灰的波纹,呼应教程主题。

What nano-banana-pro on HiAPI gives you

nano-banana-pro is an image generation model available through HiAPI's unified API. Instead of integrating with a model provider directly, you send one request shape to HiAPI and get generated images back — the same authentication, the same endpoint conventions, and the same response envelope you use for other models on the platform.

nano-banana-pro is built for prompt-driven image generation with optional reference images supplied alongside the text prompt. Reference images let you steer the output with visual context (a subject, a style, a composition) rather than describing everything in words. Outputs are delivered at 2K resolution, which makes the model a reasonable fit for production assets rather than quick drafts.

By the end of this tutorial you will have a complete, working integration that:

  • sends a prompt to nano-banana-pro through HiAPI with a verified curl request;
  • reproduces the same call in Python without changing the payload;
  • attaches reference images to the request;
  • submits an asynchronous job, polls it until it finishes, and handles retries sanely;
  • downloads the finished 2K image and stores it in your own infrastructure.

Each section builds on the previous one, so the safest path is to follow the order: confirm the request by hand first, then move it into code.

Before you send your first request

You need three things before your first call.

An API key

Create an API key in your HiAPI account and keep it out of source control. Treat it like any other secret: load it from an environment variable (HIAPI_API_KEY) or a secrets manager, never hardcode it in a file you might commit. If a key leaks, rotate it in the dashboard rather than trying to scrub it from history.

An HTTP client

Anything that can send HTTPS requests works. For the first request, curl on the command line is ideal because there is no framework between you and the wire — every header and every byte of the body is visible. For the Python section, the standard library's urllib.request is enough; if you already use requests or httpx, the mapping is the same. This tutorial keeps Python to the standard library so there is nothing extra to install.

A place to put the output

Because nano-banana-pro jobs are asynchronous, plan from the start for a directory or bucket where you will save finished images. A local output/ folder is fine for development; object storage is the right target for production.

You do not need to install a provider SDK. HiAPI is called over plain HTTPS, so the request you build in curl is the same request your Python client will send later.

A working curl request, step by step

The fastest way to confirm that your key, endpoint, and payload are all correct is to send one request by hand. Break it into its parts before you type it.

The endpoint and method

All model calls go to the same HiAPI base URL, with the model identified in the request body rather than in the path. Use POST for generation requests.

The headers

Three headers matter for every call:

  • Authorization: Bearer $HIAPI_API_KEY — your key, passed as a bearer token.
  • Content-Type: application/json — for prompt-only requests.
  • Accept: application/json — so you get a structured response rather than an HTML error page.

The body

The JSON body carries the model name and the generation parameters. At minimum you send the model identifier and your prompt:

curl -sS -X POST "$HIAPI_BASE_URL" \
  -H "Authorization: Bearer $HIAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "model": "nano-banana-pro",
    "input": {
      "prompt": "a glass terrarium on a sunlit windowsill, soft morning light, shallow depth of field"
    }
  }'

Set HIAPI_BASE_URL and HIAPI_API_KEY in your shell before running this. Keeping the URL in a variable means you can point the same script at a staging or sandbox URL later without editing the command.

Reading the response

A successful submission returns a JSON object that identifies the job. The important field is the job identifier — you will use it in the polling step. Because generation takes time, do not expect the image itself in this first response; nano-banana-pro runs asynchronously, and this call is only the submit half of the exchange.

If the command returns an error, stop here and fix it before writing any code. A verified curl request is your reference implementation: every later step should produce the same HTTP request.

Porting the request to Python

Porting is mechanical: the URL, method, headers, and JSON body stay identical. Only the client changes.

The same request with urllib

import json
import os
import urllib.request

api_key = os.environ["HIAPI_API_KEY"]
base_url = os.environ["HIAPI_BASE_URL"]

payload = {
    "model": "nano-banana-pro",
    "input": {
        "prompt": "a glass terrarium on a sunlit windowsill, soft morning light, shallow depth of field"
    },
}

request = urllib.request.Request(
    base_url,
    data=json.dumps(payload).encode("utf-8"),
    method="POST",
    headers={
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
        "Accept": "application/json",
    },
)

with urllib.request.urlopen(request, timeout=60) as response:
    body = json.loads(response.read().decode("utf-8"))

print(body)

The mapping rules

When translating from curl, three details are easy to get wrong:

  1. The body must be bytes. json.dumps produces a string; encode it before handing it to the request object.
  2. Headers are a dict, not flags. Each -H line becomes a key/value pair. Keep the Bearer prefix and the space after it.
  3. The method is explicit. urllib.request defaults to POST when data is present, but setting method="POST" keeps the intent obvious and prevents surprises if you later remove the body.

If you use requests, the equivalent is requests.post(base_url, headers=headers, json=payload, timeout=60) — json= handles serialization and the content type for you. If you use httpx, the call is the same shape.

A small helper worth having

Put the submit call in a function that takes a prompt and returns the job identifier. Every later section — reference images, polling, download — builds on that function, and keeping it in one place means a change to the request shape only happens once.

Sending reference images with your prompt

Reference images change how the request is shaped, so decide early which input style your application will use.

Two practical options

URL-based inputs. If the reference image is already reachable over HTTPS, you pass its URL in the request body alongside the prompt. This keeps the request as application/json, which is the simplest thing to retry and log.

Uploaded files. If the image lives on the client or in private storage, you send it as a file part in a multipart/form-data request. In that case the Content-Type: application/json header is replaced by the multipart content type — let your HTTP client generate it so the boundary is correct — and the JSON fields become form fields in the same request.

For most server-side integrations, URL-based inputs are easier: your images are already in object storage, the request stays JSON, and you avoid sending large bodies twice when you retry.

Keep prompts and references in sync

The prompt should describe what the reference image is for, not just what it contains. "Match the lighting and palette of the reference; change the subject to a ceramic vase" gives the model a job to do. A prompt that ignores the reference wastes the input.

Validate before you send

Reference images are the most common source of request errors. Check that the URL is publicly reachable (or that both sides of your system agree on signed URL expiry), that the file type is one the model accepts, and that the image is not so large that your upload times out. Do these checks in your own code first — a client-side validation failure is far cheaper to debug than a rejected request.

Handling async jobs and polling for results

nano-banana-pro generation is asynchronous. Structure your client around three separate steps: submit, poll, download. Mixing them into one function makes retries and timeouts hard to reason about.

Step 1: submit

Send the request described above and store the job identifier it returns. Persist that identifier somewhere durable — a database row or a queue message — because your process may restart between submit and completion.

Step 2: poll

Fetch the job's status using the same base URL and the same Authorization header, with the job identifier in the request. Poll on an interval rather than in a tight loop; a few seconds between checks is typical, and you should back off if several checks in a row return a pending state.

import time

TERMINAL = {"succeeded", "failed"}

def wait_for_job(job_id, timeout=300, interval=3):
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        status = get_job_status(job_id)  # same headers as submit
        if status in TERMINAL:
            return status
        time.sleep(interval)
    raise TimeoutError(f"job {job_id} did not finish in time")

Step 3: respect the states

Treat the status field as an enum. A job is either still running, finished successfully, or failed. Handle all three explicitly: on success, move to download; on failure, read the error message and decide whether the input or the prompt is at fault; on timeout, stop polling and surface the job identifier so you can investigate later.

Retries belong at two levels

Network-level retries should apply to the submit call and to each poll request, with a bounded number of attempts. Job-level retries are different: if a job fails because of a bad prompt or reference image, resubmitting the same payload will fail the same way. Only retry the job itself after changing the input.

A common production pattern is a worker that owns the polling loop, so web requests never block on generation. Submit from your API handler, hand the job identifier to the worker, and notify the client when the worker finishes.

Downloading and storing the 2K output

The job's final response points to the generated image rather than embedding it, which means downloading is a step you own.

Fetch the image

When the job reports success, the response includes the location of the output. Retrieve it with a normal GET request, then write the bytes to a file or stream them to storage. In development, save to a local directory with a predictable name built from the job identifier so you can correlate the file with the job record.

import pathlib
import urllib.request

def save_image(url, job_id, out_dir="output"):
    pathlib.Path(out_dir).mkdir(exist_ok=True)
    target = pathlib.Path(out_dir) / f"nano-banana-pro-{job_id}.png"
    with urllib.request.urlopen(url, timeout=60) as response:
        target.write_bytes(response.read())
    return target

Store it properly

In production, stream the response directly into object storage rather than writing to local disk, which keeps the behaviour consistent no matter which worker handles the job. Record the object key alongside the job identifier in your database, and check that the content type and file extension match what the model produced. If the image will be served on a web page, generate a thumbnail as part of the same pipeline so public pages never load the full-resolution 2K file.

Plan for retention

Generated outputs are your data. Decide how long you keep them, whether you need versioning, and whether the final location is something your app can regenerate if lost. If the answer is no, store the request payload as well as the image, so a future bug or model change never leaves you without a way to reproduce a result.

Common failure points and how to debug them

Most first integrations fail for a small set of reasons. Work through this checklist before assuming the model is at fault.

  • Authorization errors. The Bearer prefix is missing, the key has a trailing newline from a copied environment file, or you are using a key from a different environment than the base URL you set.
  • Content type mismatches. Sending JSON fields with a multipart header, or files with application/json, is the most common cause of rejected reference-image requests. Let the client set the header for multipart bodies.
  • Wrong method or body shape. A GET instead of a POST, or a top-level prompt instead of a nested input object, produces a validation error that looks nothing like the mistake. Compare your request byte-for-byte against the verified curl call — curl -v shows the exact headers being sent.
  • Polling too fast or not at all. Hammering the status endpoint gets you rate limited; never polling gets you nothing. Use a fixed interval with backoff and a hard timeout.
  • Expired output URLs. If you wait too long between job completion and download, a signed output URL can expire. Download promptly after success, or have a retry path that fetches a fresh location.
  • Silent failures in code. Always log the HTTP status code and the response body on error. A generic exception message from your HTTP client hides the useful part of the response.

When something is unclear, fall back to the verified curl request from the first section. If that request works and your Python call does not, the difference between the two is the bug.

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