Nano Banana Pro API Guide: JSON, YAML, Image Saving, and PDFs
Call gemini-3-pro-image through Google Interactions, save image bytes from the full response, and compile your own JSON or YAML job settings. PDFs need a document-capable model before Pro receives the verified facts and page images.
On this page

Nano Banana Pro has an official API: use gemini-3-pro-image. For a new integration with a Google-issued key, send JSON to https://generativelanguage.googleapis.com/v1beta/interactions. Google's image-generation documentation defines the request, and its Interactions guide recommends this API for new projects. The older generateContent API remains supported.
Start with the request below, save the entire response, then extract its image blocks. Application JSON and YAML are useful for reusable jobs, but must be compiled into Google's fields. A PDF needs a separate document-processing step because Pro's model card declares text and image inputs, not direct PDF input.
The examples follow documentation checked on October 7, 2026. Their JSON, Python syntax, configuration validation, and response handling were checked offline with synthetic data. They were not submitted to the live API; this does not establish your account's access, output quality, latency, or final bill.
Choose the model, key, and endpoint together
Use a key issued for your Google project from Google AI Studio, with billing and access appropriate to Pro. Keep it on your server. A provider-issued key belongs to that provider's endpoint; it is not interchangeable with a Google key.
Google's deprecation record lists the stable Pro model as generally available from May 28, 2026, and the preview's retirement on June 25. Replace gemini-3-pro-image-preview in Google-direct integrations. Nano Banana 2.1 is a separate model, not a replacement ID for this Pro request.
The current key policy distinguishes unrestricted standard keys, whose requests are rejected, from restricted standard keys, which continue to work. New AI Studio keys default to auth keys, bound to a service account and restricted to the Generative Language API. “All standard keys stopped working in September” is an incorrect shortcut. If an SDK sees both GOOGLE_API_KEY and GEMINI_API_KEY, Google's documented precedence favors GOOGLE_API_KEY; explicitly choosing the intended key prevents that ambiguity.

The project owns the shared quota. Creating another key does not multiply capacity. Check your project's actual limits before scheduling image jobs rather than treating published tier examples as your allowance.
Copy a complete official JSON request
Save this as request.json. It requests a landscape museum poster, with an explicit output format and size:
{
"model": "gemini-3-pro-image",
"input": [
{
"type": "text",
"text": "Create a landscape poster for a small astronomy museum. Show a paper-cut moon above a dark blue observatory. Include exactly the title MOON AFTER DARK and the label SATURDAY 8 PM. Leave clear space around both labels. Do not include ticket prices or sponsor logos."
}
],
"response_format": {
"type": "image",
"mime_type": "image/png",
"aspect_ratio": "16:9",
"image_size": "2K"
},
"store": false
}The fields come from Google's Interactions image request: model, input, and response_format. mime_type requests PNG, but your saver should still use the MIME returned with each image. Text in an image remains something to inspect before publishing; asking for exact lettering does not guarantee it.
For a real call, provide your Google key in the server's GEMINI_API_KEY variable and run this command in the directory containing request.json. It saves both the body and headers and prints the HTTP status. It does not retry automatically.
curl --silent --show-error \
--request POST \
"https://generativelanguage.googleapis.com/v1beta/interactions" \
--header "x-goog-api-key: $GEMINI_API_KEY" \
--header "Content-Type: application/json" \
--data-binary @request.json \
--dump-header response.headers \
--output response.json \
--write-out '%{http_code}\n'store: false makes this a single stateless request. For an edit, include the reference image as an input item with type: "image", raw base64 in data, and its mime_type; the compiler below does that from a local file. For server-managed conversations, use stored interactions and previous_interaction_id. Google says that the ID carries history, while tools, system instructions, and generation settings must be supplied again when needed. A stateless edit must carry its needed text and images itself. Interactions state documentation
Disabling stored interaction state does not prove zero data retention. Sensitive work needs an approved processing path covering logs, files, intermediaries, and safety storage; the API retention and ZDR guide handles that separate decision.
Save every image in the response
Do not stop at parts[0], search only for a URL, or assume the SDK's output_image gives every result. Google documents that shortcut as the last image. For mixed output, traverse steps, select model_output, and inspect its content. The following saver also supports native generateContent responses through a separate, explicit mode. Image output documentation
Save this complete standard-library program as save_response.py. It writes every final image block with a MIME-derived extension, keeps accompanying text in text.txt, and records where each file came from. The original JSON remains available for errors, usage, and other metadata.
import base64
import json
import sys
from pathlib import Path
EXTENSIONS = {
"image/png": ".png",
"image/jpeg": ".jpg",
"image/webp": ".webp",
}
def blocks(payload, mode):
if mode == "interactions":
for si, step in enumerate(payload.get("steps", [])):
if step.get("type") != "model_output":
continue
for pi, part in enumerate(step.get("content", [])):
yield f"steps[{si}].content[{pi}]", part
elif mode == "native":
for ci, candidate in enumerate(payload.get("candidates", [])):
for pi, part in enumerate(
candidate.get("content", {}).get("parts", [])
):
if part.get("thought") is True:
continue
origin = f"candidates[{ci}].content.parts[{pi}]"
if "inlineData" in part:
value = part["inlineData"]
yield origin, {
"type": "image",
"data": value.get("data"),
"mime_type": value.get("mimeType"),
}
elif "text" in part:
yield origin, {"type": "text", "text": part["text"]}
else:
raise ValueError("Choose interactions or native")
def save(payload, mode, directory):
if payload.get("error"):
raise ValueError("API error: " + json.dumps(payload["error"]))
directory = Path(directory)
directory.mkdir(parents=True, exist_ok=False)
images, texts = [], []
for origin, part in blocks(payload, mode):
if part.get("type") == "text":
texts.append(part.get("text", ""))
elif part.get("type") == "image":
mime = part.get("mime_type")
if mime not in EXTENSIONS:
raise ValueError(f"Unsupported returned image MIME: {mime}")
data = part.get("data")
if not isinstance(data, str) or not data:
raise ValueError(f"Missing inline image bytes at {origin}")
raw = base64.b64decode(data, validate=True)
if not raw:
raise ValueError(f"Empty image at {origin}")
name = f"image-{len(images) + 1:03d}{EXTENSIONS[mime]}"
(directory / name).write_bytes(raw)
images.append({"file": name, "mime_type": mime, "origin": origin})
(directory / "text.txt").write_text("\n\n".join(texts), encoding="utf-8")
(directory / "images.json").write_text(
json.dumps(images, indent=2), encoding="utf-8"
)
return len(images), len(texts)
if __name__ == "__main__":
if len(sys.argv) != 4:
raise SystemExit("Usage: save_response.py RESPONSE MODE NEW_DIRECTORY")
payload = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
count, texts = save(payload, sys.argv[2], sys.argv[3])
print(f"Saved {count} image blocks and {texts} text blocks")
if count == 0:
print("No image saved. Inspect the original response and text.txt.")After a successful image request, run:
python3 save_response.py response.json interactions museum-outputUse a fresh directory for each response so a new job cannot quietly overwrite an old image. Multiple returned images get separate files; no shortcut is saved twice. If the service returns a different MIME or a non-inline form, this program stops rather than inventing an extension or treating a task ID as image bytes. A zero-image response is diagnostic information, not a usable image deliverable.
Compile application JSON or YAML into the API request
An application “JSON prompt” describes your job. Google's transport JSON describes an API call. Keeping those separate lets a reviewer inspect requirements without accepting arbitrary API fields from a job file. Pro's model card lists Structured Output as unsupported: a JSON prompt object does not change that capability.
Save this job as museum.json:
{
"route": "google-interactions",
"model": "gemini-3-pro-image",
"prompt": {
"subject": "A paper-cut moon above a dark blue astronomy museum",
"required_text": ["MOON AFTER DARK", "SATURDAY 8 PM"],
"exclude": ["ticket prices", "sponsor logos"]
},
"output": {"aspect_ratio": "16:9", "image_size": "2K"},
"references": [],
"facts": []
}Its YAML equivalent is below. Quote ratios, sizes, and labels so YAML scalar conversion cannot change their types. Keep booleans as booleans and page numbers as integers in the PDF facts shown later.
route: google-interactions
model: gemini-3-pro-image
prompt:
subject: "A paper-cut moon above a dark blue astronomy museum"
required_text:
- "MOON AFTER DARK"
- "SATURDAY 8 PM"
exclude:
- "ticket prices"
- "sponsor logos"
output:
aspect_ratio: "16:9"
image_size: "2K"
references: []
facts: []Save the following as compile_job.py. JSON needs only Python's standard library. To read .yaml or .yml, the program uses PyYAML's safe_load; install that dependency in your own environment if you choose YAML. Both inputs go through the same validation. The program reads only the explicitly supplied job and reference files, writes request JSON to stdout, and makes no API call.
import base64
import json
import sys
from pathlib import Path
RATIOS = {"1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4",
"9:16", "16:9", "21:9"}
SIZES = {"1K", "2K", "4K"}
IMAGE_MIMES = {"image/png", "image/jpeg", "image/webp"}
def keys(value, expected, name):
if not isinstance(value, dict) or set(value) != set(expected):
raise ValueError(f"{name} must have exactly these fields: {expected}")
def string(value, name):
if not isinstance(value, str) or not value.strip():
raise ValueError(f"{name} must be a nonempty string")
return value
def strings(value, name):
if not isinstance(value, list):
raise ValueError(f"{name} must be a list")
return [string(item, name) for item in value]
def compile_job(job, base):
keys(job, ["route", "model", "prompt", "output", "references", "facts"], "job")
if job["route"] != "google-interactions":
raise ValueError("This compiler targets Google Interactions only")
if job["model"] != "gemini-3-pro-image":
raise ValueError("This job targets the stable Pro model")
prompt, output = job["prompt"], job["output"]
keys(prompt, ["subject", "required_text", "exclude"], "prompt")
keys(output, ["aspect_ratio", "image_size"], "output")
ratio = string(output["aspect_ratio"], "aspect_ratio")
size = string(output["image_size"], "image_size")
if ratio not in RATIOS or size not in SIZES:
raise ValueError("Unsupported Pro aspect ratio or image size")
subject = string(prompt["subject"], "subject")
labels = strings(prompt["required_text"], "required_text")
exclusions = strings(prompt["exclude"], "exclude")
sentences = ["Create an image showing " + subject + "."]
if labels:
sentences.append("Include exactly these labels: " + json.dumps(labels) + ".")
if exclusions:
sentences.append("Do not include: " + ", ".join(exclusions) + ".")
if not isinstance(job["facts"], list):
raise ValueError("facts must be a list")
for fact in job["facts"]:
keys(fact, ["text", "page", "verified"], "fact")
if type(fact["page"]) is not int or fact["page"] < 1:
raise ValueError("Fact page must be a positive integer")
if fact["verified"] is not True:
raise ValueError("Review each PDF fact before image generation")
text = string(fact["text"], "fact text")
sentences.append(f"Use this verified fact (source page {fact['page']}): {text}")
inputs = [{"type": "text", "text": " ".join(sentences)}]
refs = job["references"]
if not isinstance(refs, list) or len(refs) > 14:
raise ValueError("references must be a list with at most 14 items")
for ref in refs:
keys(ref, ["path", "mime_type"], "reference")
path = string(ref["path"], "reference path")
mime = string(ref["mime_type"], "reference MIME")
if mime not in IMAGE_MIMES:
raise ValueError("Reference must be a supported image, not a PDF")
raw = (Path(base) / path).read_bytes()
if not raw:
raise ValueError("Reference image is empty")
inputs.append({"type": "image", "mime_type": mime,
"data": base64.b64encode(raw).decode("ascii")})
return {
"model": job["model"], "input": inputs,
"response_format": {"type": "image", "mime_type": "image/png",
"aspect_ratio": ratio, "image_size": size},
"store": False,
}
def load(path):
text = path.read_text(encoding="utf-8")
if path.suffix.lower() in {".yaml", ".yml"}:
try:
import yaml
except ImportError:
raise SystemExit("YAML requires PyYAML; JSON needs no extra package")
return yaml.safe_load(text)
return json.loads(text)
if __name__ == "__main__":
if len(sys.argv) != 2:
raise SystemExit("Usage: compile_job.py JOB.json-or-yaml")
path = Path(sys.argv[1])
print(json.dumps(compile_job(load(path), path.parent), indent=2))Compile either version, then send the resulting request.json with the earlier curl command:
python3 compile_job.py museum.json > request.json
# Or, with PyYAML available:
python3 compile_job.py museum.yaml > request.jsonFor an edit, replace references: [] with a list such as [{"path": "museum.png", "mime_type": "image/png"}]. Paths are relative to the job file; the declared MIME must match the actual image bytes. This compiler checks the configuration and encodes the bytes, not the image file's internal validity or the model's eventual acceptance.
Do not send this YAML directly as application/yaml. Do not forward route, prompt, references, or facts as Google request fields. Similarly, cfg_scale, sampler names, or a generic seed option do not become supported Interactions parameters because another image system accepts them.
Turn a PDF into verified image inputs in two stages
Use a document-capable model to read the PDF, then send reviewed facts and selected page images to Pro. Google's document guide currently shows gemini-3.8-flash and an Interactions document part. Its general PDF limits are 50 MB or 1,000 pages, with the model's total context limit also applying. Those are document-processing limits, not permission to put a PDF into a Pro image part.
Stage 1: extract facts with their page references
For a PDF you are authorized to process, save this as make_pdf_request.py. It embeds the supplied file as raw base64 and asks for a small, reviewable extraction. It creates the request locally; running it does not upload the document.
import base64
import json
import sys
from pathlib import Path
if len(sys.argv) != 2:
raise SystemExit("Usage: make_pdf_request.py REPORT.pdf")
raw = Path(sys.argv[1]).read_bytes()
if not raw or len(raw) > 50_000_000:
raise SystemExit("Use a nonempty PDF within the documented size limit")
request = {
"model": "gemini-3.8-flash",
"input": [
{"type": "document", "mime_type": "application/pdf",
"data": base64.b64encode(raw).decode("ascii")},
{"type": "text", "text":
"Extract only the facts needed for a museum attendance infographic. "
"For each fact give the original label, value, unit, reporting period, "
"and PDF page number counted from 1. Quote a short supporting passage. "
"Flag unclear table headings or conflicting values. Do not infer missing values."},
],
"store": False,
}
print(json.dumps(request, indent=2))Run python3 make_pdf_request.py report.pdf > document-request.json. To submit it, use the earlier Google curl command with @document-request.json, document-response.json, and document-response.headers as the input and output filenames. Then save the returned text with the same complete parser:
python3 save_response.py document-response.json interactions document-outputZero saved images is expected for this text extraction. Read document-output/text.txt and check the labels, units, time period, and page references against the original PDF. An extraction model can misread a chart or associate a number with the wrong heading. Do not approve a fact merely because its page number looks plausible.
Stage 2: send the reviewed facts and page images to Pro
Copy museum.json to infographic.json. Change its subject and labels to suit the infographic, and replace facts with reviewed items. This fictional example illustrates the format; its attendance number is not extracted from a real report:
[
{"text": "Museum attendance in 2025 was 12,400 visitors.", "page": 3, "verified": true}
]The compiler rejects verified: false, a string such as "true", or a page number that is not a positive integer. This gate records a human decision; it does not verify the PDF on your behalf.
If a source diagram or layout matters, export or render only the needed PDF page with your existing PDF tool, save it as an image such as page-3.png, and add that file to references. Do not rename a PDF to .png. The text-and-image request can then be built and sent:
python3 compile_job.py infographic.json > request.json
# Send request.json using the Google curl command above.
# Save that call's response under a new filename and output directory.The handoff is now explicit: an actual document request, saved extraction text, reviewed facts with source pages, optional rendered page images, and a Pro image request. If your requirement is to preserve every table, footnote, and legal statement exactly, use document production tools; a generated infographic does not preserve an entire PDF losslessly.
Keep Google and provider protocols separate
For existing Google code, generateContent uses contents[].parts[] and generationConfig, and returns candidates[].content.parts[]. Interactions uses input, response_format, and steps. Choose the matching parser; do not combine those envelopes. If a native image reference fails with “Unsupported file URI type,” use the native Gemini file-reference repair guide. Suitable HTTPS image support on that route does not establish an Interactions field or a URL-context tool for Pro.
A provider can offer another billing and access path. For developers who choose LaoZhang, its published Pro documentation defines this generateContent-compatible endpoint:
Disclosure: laozhang.ai is our own service. Its prices are platform pricing, not an independent comparison.
POST https://api.laozhang.ai/v1beta/models/gemini-3-pro-image:generateContent
Authorization: Bearer YOUR_LAOZHANG_KEY
Content-Type: application/jsonSave this separate body as provider-request.json:
{
"contents": [{"role": "user", "parts": [{"text": "Create a paper-cut moon poster titled MOON AFTER DARK."}]}],
"generationConfig": {
"responseModalities": ["TEXT", "IMAGE"],
"imageConfig": {"aspectRatio": "16:9", "imageSize": "2K"}
}
}Use the provider's key and endpoint, with the saved response parsed in native mode:
curl --silent --show-error \
"https://api.laozhang.ai/v1beta/models/gemini-3-pro-image:generateContent" \
--header "Authorization: Bearer $LAOZHANG_API_KEY" \
--header "Content-Type: application/json" \
--data-binary @provider-request.json \
--output provider-response.json \
--write-out '%{http_code}\n'
python3 save_response.py provider-response.json native provider-outputIts OpenAI-compatible /v1/chat/completions path instead uses messages and is documented for fixed 1:1/1K text-to-image output. Choose the native-compatible path above when you need custom ratios or 2K/4K; an Interactions object pasted into chat completions does not enable those controls.
The provider's September 24 public documentation lists $0.09 per call and says an HTTP 200 response is charged once even if no image is present. That is a provider-owned billing rule, not Google's token price or a measured invoice. Check the live console price and applicable terms before use. This site's publisher operates LaoZhang; the examples establish the documented integration path, not a comparative quality or reliability result.

For production, match the endpoint, model identifier, credential issuer, body fields, response parser, and billing terms. If your workload needs a particular retention commitment, establish it for this provider and any upstream service before submitting that data.
Output sizes and costs that affect your implementation
Pro supports these ten aspect ratios in Google's image guide: 1:1, 2:3, 3:2, 3:4, 4:3, 4:5, 5:4, 9:16, 16:9, and 21:9. The sizes are uppercase 1K, 2K, and 4K. Do not copy Flash-only 512-pixel settings or additional ratios into the Pro configuration.
“4K” is an output tier rather than a universal square resolution. The Pro table gives 5504 × 3072 for 16:9 at 4K, compared with 2752 × 1536 at 2K and 1376 × 768 at 1K. That table currently has a “3.1 Pro Image” heading, while the guide's Pro examples and model card use gemini-3-pro-image; the heading is not a new model ID.
The same guide describes up to fourteen reference images overall, with guidance of six object, five character, and three style references. It also states a five-high-fidelity-image limitation. Treat fourteen as an input ceiling with task and fidelity conditions, not a promise that fourteen arbitrary references will all be preserved precisely. Pro's thinking is enabled by default and cannot be disabled; a generic troubleshooting tip to turn thinking off does not apply here. Generated images include SynthID.
Google's pricing table, checked October 7, lists these USD rates:
| Component | Standard | Batch |
|---|---|---|
| Text/image input, per million tokens | $2 | $1 |
| Text/thinking output, per million tokens | $12 | $6 |
| Image output, per million tokens | $120 | $60 |
| 1K/2K image-output equivalent | $0.134 rounded | $0.067 rounded |
| 4K image-output equivalent | $0.24 | $0.12 |
For Standard, 1K/2K uses 1,120 image-output tokens: 1120 × 120 / 1,000,000 = $0.1344. A hypothetical 100 such image outputs therefore cost $13.44 for the image-output component alone. Add input, text/thinking output, applicable search, additional attempts, and tax when estimating a complete budget. A reference image is documented as 560 input tokens, about $0.0011 at Standard input rates. Cost per accepted asset also depends on how many outputs you can actually use.
There is no Pro Free Tier row; an AI Studio trial does not establish free Pro API quota. Search grounding's 5,000-query monthly allowance is shared across Gemini 3.x models, with additional searches listed at $14 per 1,000 queries. A model request can issue multiple search queries, so request count is not that billing denominator. Google pricing
The pricing page contains Pro Flex and Priority rows, but the Pro model card marks those modes unsupported. Do not select a service tier on the assumption that a price row proves eligibility. Batch is a separately supported workflow; the Interactions overview does not provide an Interactions Batch endpoint, so do not invent /interactions/batch.
Diagnose the error before retrying
Keep the returned body and HTTP status together. Google's Interactions errors distinguish these cases:
| Returned code or symptom | Next action |
|---|---|
invalid_request or parameter_unknown | Check fields and types against the selected protocol; do not keep changing the prompt. |
failed_precondition or payment_required | Read the stated billing or prerequisite problem; repeated calls do not fix it. |
authentication or permission_denied | Check issuer, project, key restrictions, and model access. |
model_not_found or not_found | Confirm the endpoint and stable ID; a 404 alone does not identify one universal cause. |
rate_limit_exceeded, quota_exceeded, or too_many_requests | Identify the exhausted project limit or spending condition before choosing a delay. |
api_error, service_unavailable, or deadline_exceeded | Consider bounded backoff with jitter; after an uncertain timeout, check any existing interaction ID/result before duplicating the job. |
image_safety, image_prohibited_content, image_recitation, image_other, or generation_no_image | Inspect the safety or generation result and returned text; do not treat HTTP success as proof of an image. |
Rate limits are project-level and can include request, input-token, image, daily, and spending limits. Daily requests reset at midnight Pacific time. A new key does not reset those shared limits.
For transient errors, use a finite retry budget and preserve each attempt's status. Neither an uncertain timeout nor a 5xx proves a retry will be free. Provider HTTP-200 billing can differ from Google's rules, so keep that decision attached to the actual service contract.
FAQ
Can I send a JSON prompt directly to Nano Banana Pro?
You can serialize your application's requirements into the text prompt, but the official API still needs its documented request envelope. The compiler above maps a typed job object to that envelope. Pro does not support model Structured Output according to its model card.
Is YAML an official Gemini request format?
No. YAML is the application's editable configuration here. Parse it safely, validate the resulting types, and send compiled JSON with Content-Type: application/json. Quote "16:9" and "2K" so they remain strings.
Can Nano Banana Pro read a PDF directly?
Its model card declares text and image input. Use the document-processing route first, review the extracted facts, and pass those facts plus any rendered page images to Pro. The two-stage workflow above supplies both request-building programs and the response saver.
Why did my code save only one image?
It may be using the last-image shortcut or the first response part. Traverse all final image blocks in the matching protocol instead. The saver above processes each returned block once; asking for several images in a prompt still does not guarantee that count.
Can I reuse the same request with a gateway?
Only if that gateway explicitly supports the same protocol. LaoZhang's documented native-compatible route uses contents, generationConfig, and candidate parts with a provider-issued key; its chat route has a different body and fixed output settings. Match the documented fields and parser rather than changing only the base URL.
Sources11
External pages this guide links to, in the order they appear. Last updated Oct 7, 2026.
Sources11
External pages this guide links to, in the order they appear. Last updated Oct 7, 2026.
- 1.image-generation documentationai.google.dev/gemini-api/docs/image-generation
- 2.Interactions guideai.google.dev/gemini-api/docs/interactions-overview
- 3.model cardai.google.dev/gemini-api/docs/models/gemini-3-pro-image
- 4.Google AI Studioaistudio.google.com/apikey
- 5.deprecation recordai.google.dev/gemini-api/docs/deprecations
- 6.key policyai.google.dev/gemini-api/docs/api-key
- 7.document guideai.google.dev/gemini-api/docs/document-processing
- 8.generateContentai.google.dev/gemini-api/docs/generate-content/image-generation
- 9.pricing tableai.google.dev/gemini-api/docs/pricing
- 10.Interactions errorsai.google.dev/gemini-api/docs/api-errors
- 11.Rate limitsai.google.dev/gemini-api/docs/rate-limits





