Tutorial
How to Use nano-banana-pro via the HiAPI API: curl and Python Examples
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
curlrequest; - 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:
- The body must be bytes.
json.dumpsproduces a string; encode it before handing it to the request object. - Headers are a dict, not flags. Each
-Hline becomes a key/value pair. Keep theBearerprefix and the space after it. - The method is explicit.
urllib.requestdefaults toPOSTwhen data is present, but settingmethod="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
Bearerprefix 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
promptinstead of a nestedinputobject, produces a validation error that looks nothing like the mistake. Compare your request byte-for-byte against the verifiedcurlcall —curl -vshows 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 →-
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