GPT-6 Sol & Claude Opus 5.5 are liveGPT-6 Sol at half the price of 5.6 Sol

Nano Banana Pro API: Python Tutorial, Edits and Price

One POST, one poll, one download: a working Nano Banana Pro script.

Kiki, OmniaKey COO9 min read
On this page

Nano Banana Pro is Google's gemini-3-pro-image, and on OmniaKey you call it with one POST /v1/media/tasks. Our three 3:4 tests came back at 896x1200, 1792x2400 and 3584x4800, and every word in the poster was spelled right.

Short answer

  • Model: gemini-3-pro-image is Google's GA model id. The Vertex model card lists a release date of May 28, 2026.
  • Call: send POST https://api.omniakey.com/v1/media/tasks with an Idempotency-Key, poll GET /v1/media/tasks/{id} until the status is succeeded, then download assets[0].url.
  • Parameters: parameters.aspect_ratio and parameters.image_size; the size values are 1K, 2K and 4K, with an uppercase K.
  • Request limit: 1 MiB for the whole body, including inline Base64 image data.
  • Google's price: $0.134 per 1K/2K image and $0.24 per 4K image, from Google's Gemini API pricing page.
  • Our token counts: the three tasks reported 1,133, 1,225 and 2,480 output tokens. At $120 per million image tokens that is $0.136, $0.147 and $0.298.
  • Edits: use operation: "edit" and put the source in input.image as {data, mime_type}.

The Nano Banana Pro model page on OmniaKey also accepts gemini-3-pro-image-preview, the older snapshot id, at the same price.

Model id and what you need

gemini-3-pro-image is the id to write in your code. Google's Vertex model card lists it with launch stage GA and release date May 28, 2026. It supports 1K, 2K and 4K output.

The -preview id is the same model, not a separate one. OmniaKey's image model table lists gemini-3-pro-image-preview as "Same model, -preview snapshot id" at the same per-call rate.

You need two things:

  1. An OmniaKey API key, exported so the script can read it: export OMNIAKEY_API_KEY="your-key".
  2. Python 3. The script below uses only the standard library: urllib, json, time, uuid, base64 and pathlib. No SDK to install.

The Python script

Three steps: create the task, poll it, download the image. Save this as nano_banana.py and run it with python3 nano_banana.py.

python
import base64
import json
import os
import time
import urllib.request
import uuid
from pathlib import Path

API = "https://api.omniakey.com/v1/media/tasks"
HEADERS = {
    "Authorization": f"Bearer {os.environ['OMNIAKEY_API_KEY']}",
    "Content-Type": "application/json",
}


def call(url, payload=None, headers=None, method=None):
    body = json.dumps(payload).encode() if payload is not None else None
    req = urllib.request.Request(url, data=body, headers=headers or HEADERS, method=method)
    with urllib.request.urlopen(req, timeout=60) as resp:
        return resp.status, json.loads(resp.read())


def create_task(prompt, aspect_ratio="3:4", image_size="2K", image_path=None):
    body = {
        "model": "gemini-3-pro-image",
        "type": "image",
        "operation": "generate" if image_path is None else "edit",
        "input": {"prompt": prompt},
        "parameters": {"aspect_ratio": aspect_ratio, "image_size": image_size},
    }
    if image_path:
        encoded = base64.b64encode(Path(image_path).read_bytes()).decode("utf-8")
        body["input"]["image"] = {"data": encoded, "mime_type": "image/png"}
    headers = {**HEADERS, "Idempotency-Key": str(uuid.uuid4())}
    status, task = call(API, body, headers, "POST")
    print("created", task["id"], "HTTP", status)
    return task


def wait(task_id, interval=3, timeout=300):
    deadline = time.time() + timeout
    while time.time() < deadline:
        _, task = call(f"{API}/{task_id}")
        print(task["status"], task["progress"]["percent"])
        if task["status"] in ("succeeded", "failed", "canceled"):
            return task
        time.sleep(interval)
    # A timeout is not a failure: keep the task id and poll it again.
    raise TimeoutError(f"task {task_id} is still running")


def download(task, out_path):
    asset = task["assets"][0]
    with urllib.request.urlopen(asset["url"], timeout=120) as resp:
        Path(out_path).write_bytes(resp.read())
    print("saved", out_path, asset["mime_type"], asset["byte_size"], "bytes")


if __name__ == "__main__":
    task = create_task(
        "A vintage travel poster for the Night Train from Zürich to Milano, "
        "departing 21:40 from platform 7. A steam train crosses a stone arch "
        "bridge below snowy mountains under a deep blue night sky, with a cream "
        "border. Spell every word exactly as written.",
        aspect_ratio="3:4",
        image_size="2K",
    )
    task = wait(task["id"])
    if task["status"] != "succeeded":
        raise SystemExit(f"{task['status']}: {task.get('error')}")
    download(task, "night-train.jpg")

image_size takes 1K, 2K or 4K. Google's image guide is explicit: "You must use an uppercase 'K' … Lowercase parameters (e.g., 1k) will be rejected." Send 2K, not 2k.

Idempotency-Key is required. A fresh UUID per request is the simple rule; reuse a key only when you retry the exact same body. OmniaKey returns the original task with HTTP 200 instead of charging twice, and answers 409 if the same key arrives with a different body.

The create call returns 202 with an id. OmniaKey's media task docs describe the full lifecycle; the finished task carries the fields you read:

json
{
  "id": "task_AbMRVqzSnVh0VwBj0S7NuMUxy5sZtfHP",
  "status": "succeeded",
  "progress": {"percent": 100, "stage": "completed"},
  "usage": {"total_tokens": 1133},
  "assets": [
    {
      "kind": "image",
      "mime_type": "…",
      "url": "https://…signed-url…"
    }
  ]
}

Read assets[0].url and download it right away: the signed link lasts about an hour, and the task record lasts 24 hours. The mime_type field tells you the format; Gemini models usually return JPEG.

The same script at 1:1 and 2K, with a Chinese sign prompt, returned this:

A dark metal sign on a brick wall with three lines of Chinese text and a phone number
2K, 1:1: 2048x2048 in 47.2 s. All three Chinese lines and 010-6789 4321 came back correct; rust covers part of the first 0.

Editing an existing image

Set operation to "edit" and pass the source image in input.image as {data, mime_type}. The script above already does this when you give it an image_path:

python
task = create_task(
    "Hang this poster in a thin black frame on a warm white wall, with a brass "
    "picture light above it, a low wooden cabinet below and a trailing plant on "
    "the right. Keep the poster exactly as it is.",
    aspect_ratio="1:1",
    image_size="2K",
    image_path="night-train-2k.png",
)

The request body it sends looks like this:

json
{
  "model": "gemini-3-pro-image",
  "type": "image",
  "operation": "edit",
  "input": {
    "prompt": "Hang this poster in a thin black frame on a warm white wall…",
    "image": {"data": "…base64…", "mime_type": "image/png"}
  },
  "parameters": {"aspect_ratio": "1:1", "image_size": "2K"}
}

The example source is a PNG, so the script labels it image/png; change that label for a JPEG. For several references, use input.images and give each entry the same {data, mime_type} shape. Google's guide puts the limit at 14 reference images; for gemini-3-pro-image the split is 6 object images, 5 character images and 3 style references.

Keep the body under 1 MiB. Base64 makes the file about a third larger, so resize or compress the source before you encode it. Our edit used the 2K poster and kept every word and the layout intact:

The night-train poster in a black frame on a wall, with a picture light above it and a plant to the right
2K, 1:1 edit: 2048x2048 in 49.9 s. The poster text and design survived; the frame is a clean rectangle.

1K, 2K and 4K: what each buys

We ran the same 3:4 poster prompt at all three sizes. The pixel sizes matched Google's table for 3:4 exactly: 896x1200 at 1K, 1792x2400 at 2K and 3584x4800 at 4K. The poster text was correct at every size.

The night-train poster at 1K: steam train on a stone bridge, snowy mountains and a deep blue sky
1K, 3:4: 896x1200 in 28.4 s. All four poster lines are right; the locomotive's lamps and rods crowd together.
The same night-train poster at 2K, with a village and two spires added on the right slope
2K, 3:4: 1792x2400 in 48.2 s. Same text, plus a village and two spires on the right slope.
The same night-train poster at 4K, with three carriages and a tender drawn behind the locomotive
4K, 3:4: 3584x4800 in 66.9 s. Three carriages and the tender are drawn; the brick seams vary in thickness.

What changes with size is detail, not spelling:

  • 1K: the train front is crowded: lamps, wheels and connecting rods sit close together.
  • 2K: the village and its two spires appear on the right slope, the chimney gets a pale thread of smoke, and the bridge shows seven arches.
  • 4K: the three carriages and the tender are separate, and the stonework shows brick seams of different thickness. The machinery under the locomotive is still simplified.

Speed went up with size: 28.4 s, 48.2 s and 66.9 s. We ran each size once, so read those seconds as a single run.

Google prices image output at $120 per million tokens, and its table gives 1,120 tokens for both 1K and 2K and 2,000 tokens for 4K. That works out to Google's own estimate:

text
1,120 × $120 ÷ 1,000,000 = $0.134 per 1K image
1,120 × $120 ÷ 1,000,000 = $0.134 per 2K image
2,000 × $120 ÷ 1,000,000 = $0.24  per 4K image

Our tasks reported more tokens than Google's examples. Here is the same arithmetic on our counts:

text
1,133 × $120 ÷ 1,000,000 = $0.136 per 1K image
1,225 × $120 ÷ 1,000,000 = $0.147 per 2K image
2,480 × $120 ÷ 1,000,000 = $0.298 per 4K image
SizePixels, 3:4Our timeOur tokensGoogle estimate
1K896x120028.4 s1,133$0.134
2K1792x240048.2 s1,225$0.134
4K3584x480066.9 s2,480$0.24

OmniaKey bills an image call at a flat rate, not by token, so its charge does not move with those counts; the model page has the current per-call rate, and a failed task is refunded. Google's token-rate estimate for the same three images is $0.134, $0.134 and $0.24.

Errors you will hit

The failures listed here mostly come down to a field name or a file size, not the model. These are the ones OmniaKey's media docs list for this endpoint:

HTTPCode or statusWhat it meansWhat to do
400invalid_requestMalformed body, unsupported operation or bad parameterCheck the JSON and the field names below
503no_available_channelNo channel currently serves this modelSwitch models; retrying will not fix it
—failedThe job finished without a resultRead error.code and error.message

A failed task is not an HTTP error: the create call succeeded, so poll the task and read its error object. Failed and canceled jobs are refunded automatically.

A full-resolution source sent as Base64 can push the body over 1 MiB. Resize first, and check the encoded length, not just the file size.

Google's guide rejects a lowercase 1k. Use 1K, 2K or 4K.

OmniaKey's image docs also reject size and a top-level mime_type on Gemini models, because those are not Gemini parameters. Use parameters.aspect_ratio and parameters.image_size instead, and read the output format from the response's mime_type field.

Common questions

What is the Nano Banana Pro API model id?

gemini-3-pro-image. Google's Vertex model card lists it as GA with a release date of May 28, 2026, and OmniaKey accepts the same id. The older gemini-3-pro-image-preview id still works on OmniaKey and points to the same model.

How much does one Nano Banana Pro image cost?

Google estimates $0.134 for a 1K or 2K image and $0.24 for a 4K image at its $120 per million image-token rate. Our tasks reported 1,133, 1,225 and 2,480 tokens, which comes to $0.136, $0.147 and $0.298 at that rate. OmniaKey's model page lists its own per-call rate.

Can Nano Banana Pro render Chinese text?

Yes. Our 2K sign came back with three correct Chinese lines — 同福客栈, 住宿 餐饮 茶水, and 010-6789 4321 — including all 11 digits and the hyphen.

Should I request 1K, 2K or 4K?

For a poster like ours, 2K is the useful middle. All three sizes spelled our poster correctly, so 4K buys finer detail — separate carriages and brick seams — rather than better text. Use 1K for drafts where 896x1200 is enough.

Why did my edit return 400 invalid_request?

The usual causes are a body over 1 MiB, the source in the wrong field, or a Gemini model receiving size or a top-level mime_type. Put the image in input.image as {data, mime_type}, keep the encoded body under 1 MiB, and use aspect_ratio and image_size.

Sources checked

Update log

  • 2026-11-03: First version; five test calls run through OmniaKey that day.

Models in this article

Nano Banana ProGemini imageimage APIPythonAPI tutorial

Related articles

Recent posts

Popular guides