怎么调用 Nano Banana Pro API:完整走查
这是在 E2X API 上调用 Nano Banana Pro 的整套集成,从头到尾。拿一个 key、提交一个 job、读 response、用对的方式等待、处理它可能失败的那些方式,然后把字节放到一个明天还在的地方。
最后那一步是绊住人的那一步。如果您已经在生成图像、只想知道自己的图为什么不见了,就直接跳到那里。

Pro 是不是适合您工作的那一档,是另一个问题,我们单独回答过,在价格与速度那一篇里。短版本:每次 request $0.075,大约 30 秒,如果您的图像带着可读的文字,或者要把扮演不同角色的参考图组合起来,那就值。这一页假设您已经决定了。
发出第一次 request 之前需要什么
四样东西,其中三样各只有一行。
- 一个 API key。 从您的 E2X 账户里创建。下面的一切都以
Authorization: Bearer $E2X_API_KEY发送它。把它留在服务端。一个出现在浏览器 JavaScript 里的 key,是一个别人正在花的 key。 - base URL,对目录里的每一个模型都是
https://api.e2x.ai/v1。 - 正确的 slug。 生成是
google/nano-banana-pro/text-to-image。编辑是google/nano-banana-pro/edit-image。这些字符串一字不差地放进 request body,它们不做模糊匹配。 - 一条值得发出去的 prompt。 Pro 对具体性的回报比 flash 那几档更高,因为它有更多余地去执行。如果您想比「一只猫,但要电影感」走得更深,我们有一份单独的 prompt 指南。
这份契约是异步的。您提交一个 job,拿到一个 ID,图像稍后到达。没有一个会阻塞到像素就绪的同步 endpoint,而考虑到 Pro 要花大约 30 秒,您也不会想要那样一个。
您的第一次 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": "大理石咖啡吧台后面一块手写字体的黑板菜单,温暖的午后光线从左侧窗户进来,浅景深",
"aspect_ratio": "16:9",
"resolution": "2k"
}
}'
同样的东西,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"]
两个例子都刻意钉死了 aspect_ratio 和 resolution,而在您把它们删掉之前,这两个默认值都值得搞清楚。
aspect_ratio 默认是 9:16。 不是 1:1。如果您把它留空,您生成的每一张图都是竖版,永远如此,而您要等到版式里某个宽格子看着不对劲时才会发现。在我们的 API 上,这个字段绊倒的集成比任何其他字段都多。把它钉死。
resolution 的取值是小写的。 1k、2k、4k。大写的 2K 不是同一个字符串,API 会这么告诉您。
在这个 slug 上,请把 1k 当作一个废值。前两档设置落在同一笔固定收费上——两者都是 $0.075,核查日期 2026 年 8 月 26 日——所以更小的那个只是用同一行账单换回更少的像素。只有 4k 会动那个数字:$0.15,上限 4096×4096。

回来的是什么
submit 调用会立刻返回一个 job 信封。您需要的字段是 data.jobId:
{
"success": true,
"data": {
"jobId": "job_8Kd2mQvXpL",
"status": "pending"
}
}
状态从 pending → processing → completed,或者落在两种终态失败之一上,failed 和 cancelled。一个完成的 job 在 data.outputs[0].url 带着它的结果,一个失败的 job 在 data.error.message 带着原因。
在您写任何计费代码之前,有一个细节要内化:API 返回的每一个金额都以微分(micro-cent)计。 一百万等于一美元。所以一次 Nano Banana Pro request 回来的是 75000,不是 0.075。只在展示层除以 1,000,000,别在任何其他地方除,也永远不要存那个除完的数。
做 polling,但别捶它
粗暴的版本管用:
curl https://api.e2x.ai/v1/jobs/job_8Kd2mQvXpL \
-H "Authorization: Bearer $E2X_API_KEY"
把它包进一个固定两秒睡眠的循环里,您就有了一个能跑的集成。具体到 Nano Banana Pro,固定间隔其实完全可以接受——一次 request 大约跑 30 秒,所以您会打大约十五次状态查询然后停下。那点流量惊扰不到任何人。
退避(backoff)仍然更好,理由跟礼貌毫无关系。固定间隔会藏住方差。如果某个 job 花了 90 秒而不是 30 秒,固定循环仍然按同样的频率调用,您的日志看起来跟一次健康的运行一模一样。一个会增长的退避,会让一个慢 job 显得明显地慢。
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")
那个循环做了两件天真版本通常不做的事。它有一个硬性截止时间,所以一个卡住的 job 会抛异常,而不是一直空转到进程被杀掉。以及它在传输层对三种终态一视同仁,把 payload 返回去,让调用方去决定一次失败意味着什么。重试逻辑属于这个函数之上,不属于它内部。

Webhook,以及您为什么该转过去
在 submit body 里传一个 webhookUrl,job 抵达终态时我们会呼叫您。完全不需要 polling 循环。
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": "制图桌上一座人行天桥的建筑模型,北向天光",
"aspect_ratio": "3:2",
"resolution": "2k"
},
"webhookUrl": "https://your-app.example.com/hooks/e2x"
}'
polling 是正确的第一步,因为您能在终端里十秒钟就把它试出来。它是错误的稳定状态,理由是算术。一次一张图,polling 的代价是一个循环。一批两百张图,polling 的代价是两百个并发循环,每一个都攥着一条连接半分钟,而这个进程现在一重启就会丢掉所有在飞 job 的踪迹。
webhook 让这件事变得可重启。提交时把 job ID 写进您的数据库,我们呼叫时处理器更新那一行,中间来一次部署什么都不会变。如果您在搭的是一条生成流水线而不是一个脚本,那就该搭这个版本,而它和我们那篇端到端自动化图像生成里的模式是配套的。
两点运维提醒。您的 endpoint 必须能从公网访问,所以一个 localhost URL 在开发期间会悄无声息地永远不触发——用一条隧道。以及,请把 webhook 当作一条通知,而不是事实来源:在处理器内部先按 ID 取回那个 job,再据此行动。
编辑一张图像,而不是生成一张
同一个信封,不同的 slug,多一个字段。edit endpoint 在 prompt 之外接受 image_urls,价格同样是 $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": "把背景换成柔和的灰色棚拍背景纸,产品的打光完全保持原样",
"image_urls": ["https://your-cdn.example.com/source/bottle.jpg"],
"aspect_ratio": "1:1",
"resolution": "2k"
}
}'
您传入的 URL 必须是公开可取的。一条来自您自己存储的签名 URL 管用;一条您笔记本上的路径不管用。
Nano Banana Pro 最多接受十四张参考图,而且和其他档位不同,它按角色把它们分开——最多 5 张管角色身份,最多 6 张管物体还原,最多 3 张管风格。那套角色分离正是待在这一档上的理由,而按角色限定的参考图的确切字段名,在每个模型的机器可读规格文件里都有记载,如果我们的文档和这篇文章哪天说法不一致,以它为准。具体到角色一致性的工作流,我们在一致生成指南里讲得更深。
每一个参数,以及它的默认值
| 字段 | 放在哪里 | 接受的取值 | 默认值 |
|---|---|---|---|
model | body 根部 | google/nano-banana-pro/text-to-image 或 google/nano-banana-pro/edit-image | 必填 |
input.prompt | input 内部 | 字符串 | 必填 |
input.aspect_ratio | 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 | input 内部 | 1k、2k、4k | 请显式钉死 |
input.image_urls | input 内部,仅 edit slug | 一组公开可取的 URL | edit 上必填 |
webhookUrl | body 根部 | 一个公开的 HTTPS endpoint | 无,改用 polling |
会让人花钱的是 aspect_ratio 和 resolution 这两行。其余的行为都和您猜的一样。
当 job 没有完成时
三个失败面,它们需要不同的处理。
submit 调用本身失败。 这是在任何 job 存在之前的一个 HTTP 错误——key 不对、body 格式错误、模型 slug 不认识。什么都没入队,什么都没计费。修好这个 request;原样重试会以同样的方式失败。
job 抵达 failed。 job 存在过,而模型没有产出图像。去 data.error.message 读原因,通常是一次内容政策拒绝,或者一处格式错误的输入,比如 image_urls 里有一个取不到的 URL。对一次政策拒绝做盲目重试,会以同样的方式失败;在一次瞬时上游错误之后重试,通常会成功。请把消息记进日志,别只记状态。
job 抵达 cancelled。 这是终态,不是 bug。在代码层面,请完全按 failed 来处理——这一行结束了,不会再有输出。
您的等待循环超时了。 这不是一个 job 状态。它意味着 job 还在跑,而您的耐心耗尽了。那个 job ID 依然有效,所以请把它记下来稍后再查,而不是重新提交——那会让您为同一张图付两次钱。
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"]
在输出过期之前把它下载下来
正是这一部分会在上线一周后交出一堆碎图,所以它单开一节。
data.outputs[0].url 那个 URL 是临时的。 它是一个交付 URL,不是托管。如果您把那个字符串写进一个叫 image_url 的数据库列,然后在商品页上渲染它,这个页面在预发环境里能用、在评审时能用、上线当天能用,然后在那个对象到期之后悄悄变成一堆碎图标。
修法只有一步,而且不是可选的。取回字节,放进您自己的存储,存您自己的 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
或者,在 shell 里:
curl -sL "$OUTPUT_URL" -o ./out/menu-board.jpg
请把下载放在处理完成事件的同一个工作单元里做。不要放在一小时后的 cron 上,也不要懒到首次页面访问时才做。这个窗口宽到足以让您在测试时侥幸过关,又窄到让您在压力下无法侥幸。

整件事,写成一个脚本
上面的一切,拼起来。设好 E2X_API_KEY 然后运行它。
#!/usr/bin/env python3
"""用 Nano Banana Pro 生成一张图像并存到本地。"""
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(
"大理石咖啡吧台后面一块手写字体的黑板菜单,"
"温暖的午后光线从左侧窗户进来,浅景深"
)
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()
把 slug 换成 google/nano-banana-pro/edit-image 再加上 image_urls,同一个脚本就从生成变成了编辑。把它换成 Nano Banana 2 或 Nano Banana 2 Lite,它照样跑,因为 text-to-image 分类里的一切和 image-to-image 里的一切共用同一个信封。不同的只有 input 字段,而模型对比那一篇讲了该把它指向哪一个。
最后一件不是代码的事。每一个 Google 模型出的每一张图像都带着 SynthID watermark,而没有任何提供商能禁用它。请在往上面搭东西之前,跟签合同的那个人把这件事定下来。
常见问题
我该怎么调用 Nano Banana Pro API?
向 https://api.e2x.ai/v1/jobs/submit 发一个 POST,带上 bearer token、模型 slug google/nano-banana-pro/text-to-image,以及一个装着您 prompt 的 input 对象。response 会返回 data.jobId。对 https://api.e2x.ai/v1/jobs/{id} 做 polling,或者在 submit body 里传一个 webhookUrl,然后从 data.outputs[0].url 读取完成的图像。
Nano Banana Pro 的默认 aspect ratio 是什么?
9:16,也就是竖版。这几乎让所有人意外,因为大多数图像 API 默认方图。如果您不显式设置 aspect_ratio,每一张图出来都是竖的。这个模型接受 9:16、16:9、1:1、2:3、3:2、21:9、3:4、4:3、4:5 和 5:4。
为什么我的 Nano Banana Pro 图像 URL 坏了?
因为我们返回的输出 URL 是临时交付,不是永久托管。任何把我们的 URL 存进数据库、之后再渲染的流水线,都会在那个对象过期之后显示碎图。请在处理 job 完成的同一步里下载字节,并把它们存进您自己的 bucket。
用 E2X API 该选 polling 还是 webhook?
搭建期间用 polling,因为您能在终端里试它。任何跑在生产里的东西都请转到 webhook。一个 Nano Banana Pro job 大约要 30 秒,所以一批两百张就意味着两百个并发 polling 循环,而它们会在下一次部署时全部丢掉状态。用 webhookUrl,job ID 在提交时就进了您的数据库,处理器稍后把它接上。
我怎么用 Nano Banana Pro 编辑一张现有图像?
用 slug google/nano-banana-pro/edit-image,并在 input 对象里、prompt 之外加上一个 image_urls 数组。那些 URL 必须是公开可取的,所以一条来自您存储的签名 URL 管用,而一条本地路径不管用。编辑和生成同价,都是 $0.075。
我该向 Nano Banana Pro 请求什么分辨率?
发 2k。在这个 slug 上前两档设置共用一笔固定收费,所以 1k 是严格劣势的——像素更少,账单那一行一模一样。只有当您确实需要 4096×4096 这个上限时才去要 4k,因为它会把费用翻倍到 $0.15。三个值全是小写。
为什么 E2X API 返回的价格是 75000 这样的数字?
API 里的每一个金额都以微分表示,1,000,000 等于一美元。一个 Nano Banana Pro job 上的 75000 就是 $0.075。请在存储里保留那个整数,只在人要读它的那一刻做除法,这样舍入误差就永远不会在一个计费周期里累积起来。