How to Call the Nano Banana Pro API: A Complete Walkthrough
This is the whole integration, start to finish, for Nano Banana Pro on the E2X API. Get a key, submit a job, read the response, wait for it properly, handle the ways it can fail, and put the bytes somewhere that will still be there tomorrow.
That last step is the one that catches people. Skip to it if you are already generating images and just want to know why yours went missing.

Whether Pro is the right tier for your work is a different question and we answered it separately, in the pricing and speed post. Short version: $0.075 a request, about 30 seconds, worth it if your images carry readable text or combine references that play different roles. This page assumes you have decided.
What you need before the first request
Four things, and three of them are one line each.
- An API key. Create one from your E2X account. Everything below sends it as
Authorization: Bearer $E2X_API_KEY. Keep it server-side. A key in browser JavaScript is a key someone else is now spending. - The base URL, which is
https://api.e2x.ai/v1for every model in the catalog. - The right slug. Generation is
google/nano-banana-pro/text-to-image. Editing isgoogle/nano-banana-pro/edit-image. Those strings go in the request body verbatim, and there is no fuzzy matching on them. - A prompt worth sending. Pro rewards specificity more than the flash tiers do, because it has more room to act on it. We have a separate prompting guide if you want to go deeper than "a cat, but cinematic".
The contract is asynchronous. You submit a job, you get an ID, and the image arrives later. There is no synchronous endpoint that blocks until the pixels are ready, and given that Pro takes around 30 seconds, you would not want one.
Your first text-to-image request
curl -X POST https://api.e2x.ai/v1/jobs/submit \
-H "Authorization: Bearer $E2X_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "google/nano-banana-pro/text-to-image",
"input": {
"prompt": "A hand-lettered chalkboard menu behind a marble coffee counter, warm afternoon light from a window on the left, shallow depth of field",
"aspect_ratio": "16:9",
"resolution": "2k"
}
}'
The same thing in Python:
import os
import requests
BASE = "https://api.e2x.ai/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['E2X_API_KEY']}"}
def submit(prompt, aspect_ratio="16:9", resolution="2k"):
r = requests.post(
f"{BASE}/jobs/submit",
headers=HEADERS,
json={
"model": "google/nano-banana-pro/text-to-image",
"input": {
"prompt": prompt,
"aspect_ratio": aspect_ratio,
"resolution": resolution,
},
},
timeout=30,
)
r.raise_for_status()
return r.json()["data"]["jobId"]
Both examples pin aspect_ratio and resolution on purpose, and both defaults are worth understanding before you drop them.
aspect_ratio defaults to 9:16. Not 1:1. If you leave it out, every image you generate is portrait, forever, and you will not notice until a wide slot in your layout looks wrong. This trips up more integrations than any other field on our API. Pin it.
resolution values are lowercase. 1k, 2k, 4k. Uppercase 2K is not the same string and the API will tell you so.
Treat 1k as a dead value on this slug. The first two settings sit on one flat charge — $0.075 for either, checked 26 August 2026 — so the smaller one just returns fewer pixels for the same invoice line. Only 4k moves the number: $0.15, ceiling 4096×4096.

What comes back
The submit call returns immediately with a job envelope. The field you need is data.jobId:
{
"success": true,
"data": {
"jobId": "job_8Kd2mQvXpL",
"status": "pending"
}
}
Status moves pending → processing → completed, or lands on one of two terminal failures, failed and cancelled. A completed job carries its result at data.outputs[0].url, and a failed one carries a reason at data.error.message.
One detail to internalise before you write any billing code: every monetary value the API returns is in micro-cents. One million equals one dollar. A Nano Banana Pro request therefore comes back as 75000, not 0.075. Divide by 1,000,000 at the display layer and nowhere else, and never store the divided number.
Polling without hammering
The blunt version works:
curl https://api.e2x.ai/v1/jobs/job_8Kd2mQvXpL \
-H "Authorization: Bearer $E2X_API_KEY"
Wrap that in a loop with a fixed two-second sleep and you have a functioning integration. For Nano Banana Pro specifically, a fixed interval is genuinely acceptable — a request runs about 30 seconds, so you make roughly fifteen status calls and stop. That is not enough traffic to bother anyone.
A backoff is still better, for one reason that has nothing to do with politeness. Fixed intervals hide variance. If a job takes 90 seconds instead of 30, a fixed loop keeps calling at the same rate and your logs look identical to a healthy run. A backoff that grows makes a slow job visibly slow.
import time
TERMINAL = {"completed", "failed", "cancelled"}
def wait_for(job_id, timeout=300):
delay = 2.0
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
r = requests.get(f"{BASE}/jobs/{job_id}", headers=HEADERS, timeout=30)
r.raise_for_status()
data = r.json()["data"]
if data["status"] in TERMINAL:
return data
time.sleep(delay)
delay = min(delay * 1.4, 10.0)
raise TimeoutError(f"{job_id} still running after {timeout}s")
Two things that loop does which a naive one usually does not. It has a hard deadline, so a stuck job raises instead of spinning until the process is killed. And it treats all three terminal states the same way at the transport layer, returning the payload and letting the caller decide what a failure means. Retry logic belongs above this function, not inside it.

Webhooks, and why you should move to them
Pass webhookUrl in the submit body and we call you when the job reaches a terminal state. No polling loop at all.
curl -X POST https://api.e2x.ai/v1/jobs/submit \
-H "Authorization: Bearer $E2X_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "google/nano-banana-pro/text-to-image",
"input": {
"prompt": "An architectural model of a footbridge on a drafting table, north light",
"aspect_ratio": "3:2",
"resolution": "2k"
},
"webhookUrl": "https://your-app.example.com/hooks/e2x"
}'
Polling is the right first move because you can test it from a terminal in ten seconds. It is the wrong steady state, and the reason is arithmetic. One image at a time, polling costs you a loop. Two hundred images in a batch and polling costs you two hundred concurrent loops, each holding a connection for half a minute, in a process that now cannot be restarted without losing track of every job in flight.
Webhooks make the work restartable. The job ID goes in your database when you submit, the handler updates the row when we call, and a deploy in the middle changes nothing. If you are building a generation pipeline rather than a script, this is the version to build, and it pairs with the patterns in our post on automating image generation end to end.
Two operational notes. Your endpoint must be reachable from the public internet, so a localhost URL will silently never fire during development — use a tunnel. And treat the webhook as a notification, not as the source of truth: fetch the job by ID inside the handler before you act on it.
Editing an image instead of generating one
Same envelope, different slug, one extra field. The edit endpoint takes image_urls alongside the prompt and costs the same $0.075.
curl -X POST https://api.e2x.ai/v1/jobs/submit \
-H "Authorization: Bearer $E2X_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "google/nano-banana-pro/edit-image",
"input": {
"prompt": "Replace the background with a soft grey studio sweep, keep the product lighting exactly as it is",
"image_urls": ["https://your-cdn.example.com/source/bottle.jpg"],
"aspect_ratio": "1:1",
"resolution": "2k"
}
}'
The URLs you pass must be publicly fetchable. A signed URL from your own storage works; a path on your laptop does not.
Nano Banana Pro accepts up to fourteen reference images, and unlike the other tiers it separates them by role — up to 5 for character identity, up to 6 for object fidelity, up to 3 for style. That role split is the reason to be on this tier at all, and the exact field names for role-scoped references are documented per model in the machine-readable spec file, which is the authority if our docs and this post ever disagree. For the character-consistency workflow specifically, we go deeper in the consistent generation guide.
Every parameter, and its default
| Field | Where it goes | Accepted values | Default |
|---|---|---|---|
model | body root | google/nano-banana-pro/text-to-image or google/nano-banana-pro/edit-image | required |
input.prompt | inside input | string | required |
input.aspect_ratio | inside input | 9:16, 16:9, 1:1, 2:3, 3:2, 21:9, 3:4, 4:3, 4:5, 5:4 | 9:16 |
input.resolution | inside input | 1k, 2k, 4k | pin it explicitly |
input.image_urls | inside input, edit slug only | array of publicly reachable URLs | required on edit |
webhookUrl | body root | a public HTTPS endpoint | none, poll instead |
The two rows that cost people money are aspect_ratio and resolution. Everything else behaves the way you would guess.
When the job does not complete
Three failure surfaces, and they need different handling.
The submit call itself fails. This is an HTTP error before any job exists — a bad key, a malformed body, an unknown model slug. Nothing was queued and nothing was charged. Fix the request; retrying it unchanged will fail identically.
The job reaches failed. The job existed and the model did not produce an image. Read data.error.message for the reason, which is usually a content policy refusal or a malformed input such as an unreachable URL in image_urls. A blind retry of a policy refusal fails the same way; a retry after a transient upstream error usually succeeds. Log the message, do not just log the status.
The job reaches cancelled. Terminal, and not a bug. Treat it exactly like failed at the code level — the row is closed, no output is coming.
Your wait loop times out. Not a job state. It means the job is still running and your patience ran out. The job ID is still valid, so record it and check again later rather than resubmitting, which would charge you twice for the same image.
def generate(prompt, **kw):
job_id = submit(prompt, **kw)
job = wait_for(job_id)
if job["status"] != "completed":
reason = (job.get("error") or {}).get("message", "no reason given")
raise RuntimeError(f"{job_id} ended as {job['status']}: {reason}")
return job["outputs"][0]["url"]
Download the output before it expires
This is the part that ships broken images a week after launch, so it gets its own section.
The URL at data.outputs[0].url is temporary. It is a delivery URL, not hosting. If you write that string into a database column called image_url and render it on a product page, the page works in staging, works in review, works on launch day, and then quietly turns into broken image icons once the object ages out.
The fix is one step and it is not optional. Fetch the bytes, put them in your own storage, store your URL.
import pathlib
def download(url, dest):
with requests.get(url, stream=True, timeout=120) as r:
r.raise_for_status()
pathlib.Path(dest).parent.mkdir(parents=True, exist_ok=True)
with open(dest, "wb") as f:
for chunk in r.iter_content(chunk_size=1 << 16):
f.write(chunk)
return dest
Or, from the shell:
curl -sL "$OUTPUT_URL" -o ./out/menu-board.jpg
Do the download inside the same unit of work that handled completion. Not on a cron an hour later, not lazily on first page view. The window is generous enough that you will get away with a delay in testing and narrow enough that you will not get away with it under load.

The whole thing, as one script
Everything above, assembled. Set E2X_API_KEY and run it.
#!/usr/bin/env python3
"""Generate one image with Nano Banana Pro and store it locally."""
import os
import pathlib
import time
import requests
BASE = "https://api.e2x.ai/v1"
MODEL = "google/nano-banana-pro/text-to-image"
HEADERS = {"Authorization": f"Bearer {os.environ['E2X_API_KEY']}"}
TERMINAL = {"completed", "failed", "cancelled"}
def submit(prompt, aspect_ratio="16:9", resolution="2k"):
r = requests.post(
f"{BASE}/jobs/submit",
headers=HEADERS,
json={
"model": MODEL,
"input": {
"prompt": prompt,
"aspect_ratio": aspect_ratio,
"resolution": resolution,
},
},
timeout=30,
)
r.raise_for_status()
return r.json()["data"]["jobId"]
def wait_for(job_id, timeout=300):
delay = 2.0
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
r = requests.get(f"{BASE}/jobs/{job_id}", headers=HEADERS, timeout=30)
r.raise_for_status()
data = r.json()["data"]
if data["status"] in TERMINAL:
return data
time.sleep(delay)
delay = min(delay * 1.4, 10.0)
raise TimeoutError(f"{job_id} still running after {timeout}s")
def download(url, dest):
with requests.get(url, stream=True, timeout=120) as r:
r.raise_for_status()
pathlib.Path(dest).parent.mkdir(parents=True, exist_ok=True)
with open(dest, "wb") as f:
for chunk in r.iter_content(chunk_size=1 << 16):
f.write(chunk)
return dest
def main():
job_id = submit(
"A hand-lettered chalkboard menu behind a marble coffee counter, "
"warm afternoon light from a window on the left, shallow depth of field"
)
print("submitted", job_id)
job = wait_for(job_id)
if job["status"] != "completed":
reason = (job.get("error") or {}).get("message", "no reason given")
raise SystemExit(f"{job_id} ended as {job['status']}: {reason}")
path = download(job["outputs"][0]["url"], "out/menu-board.jpg")
print("saved", path)
if __name__ == "__main__":
main()
Swap the slug for google/nano-banana-pro/edit-image and add image_urls and the same script edits instead of generates. Swap it for Nano Banana 2 or Nano Banana 2 Lite and it still runs, because the envelope is identical across everything in the text-to-image category and everything in image-to-image. Only the input fields differ, and the model comparison post covers which one to point it at.
One last thing that is not code. Every image from every Google model carries a SynthID watermark and no provider can disable it. Settle that with whoever signs the contract before you build on top of it.
Frequently asked questions
How do I call the Nano Banana Pro API?
Send a POST to https://api.e2x.ai/v1/jobs/submit with a bearer token, the model slug google/nano-banana-pro/text-to-image, and an input object containing your prompt. The response returns data.jobId. Poll https://api.e2x.ai/v1/jobs/{id} or pass a webhookUrl in the submit body, then read the finished image from data.outputs[0].url.
What is the default aspect ratio for Nano Banana Pro?
9:16, which is portrait. This surprises almost everyone, because most image APIs default to square. If you do not set aspect_ratio explicitly, every image comes out vertical. The model accepts 9:16, 16:9, 1:1, 2:3, 3:2, 21:9, 3:4, 4:3, 4:5 and 5:4.
Why is my Nano Banana Pro image URL broken?
Because the output URL we return is temporary delivery, not permanent hosting. Any pipeline that saves our URL into a database and renders it later will show broken images once the object expires. Download the bytes in the same step that handles job completion and store them in your own bucket.
Should I use polling or webhooks with the E2X API?
Poll while you are building, because you can test it from a terminal. Move to webhooks for anything running in production. A Nano Banana Pro job takes about 30 seconds, so a batch of two hundred means two hundred concurrent polling loops that all lose their state on the next deploy. With webhookUrl the job ID goes in your database at submit time and the handler picks it up later.
How do I edit an existing image with Nano Banana Pro?
Use the slug google/nano-banana-pro/edit-image and add an image_urls array to the input object alongside your prompt. The URLs must be publicly fetchable, so a signed URL from your storage works but a local path does not. Editing costs the same $0.075 as generation.
What resolution should I request from Nano Banana Pro?
Send 2k. The first two settings share one flat charge on this slug, so 1k is strictly dominated — fewer pixels, identical invoice line. Reach for 4k only when you genuinely need the 4096×4096 ceiling, since it doubles the charge to $0.15. All three values are lowercase.
Why does the E2X API return prices like 75000?
Every monetary value in the API is expressed in micro-cents, where 1,000,000 equals one US dollar. A 75000 on a Nano Banana Pro job is $0.075. Keep the integer in storage and divide only at the point where a human reads it, so rounding never accumulates across a billing period.