04Getting started
Requests in the Gemini API formatNew
Nano Banana models accept requests in Google’s Gemini API format: the same routes, the same request body and the same shape of response. Only the address and the key change.
What this is and the routes
This is a separate path for the Nano Banana models. It accepts requests in Google’s Gemini API format at the root of the host https:// and passes them through the same request handling as the regular API (/api/): the checks, prices, limits and balance are the same.
| Route | What it does |
|---|---|
POST /v1beta/ | Creates an image and returns one response. The recommended route. |
POST /v1beta/ | The same, but as a streamed response. With ?alt=sse you get SSE events; without it, Google’s JSON array with one response in it. |
GET /v1beta/ | The list of models your key can call. |
GET /v1beta/ | One model. |
Under /v1 only two POST routes work: POST /v1/ and POST /v1/. Every other /v1 path answers as an unknown path.
Key
You can pass your Rendergrid key in three ways: in the x-goog-api-key header, in the Authorization: Bearer header, or in the ?key= URL parameter. The server takes the first non-empty one in this order. The key does not get into our logs.
A revoked, expired, disabled or blocked key behaves as in the regular API: see “Keys and authentication”.
Examples
In the examples the key is read from the environment variable RENDERGRID_API_KEY. The image comes back in the response as base64, in the inlineData.data field.
For Python and Node.js we use Google’s Gen AI SDK: pip install google-genai and npm install @google/genai. The client is created with our address https:// and your Rendergrid key. The Node.js example is an ES module (a .mjs file): await stands at the top level in it.
One response — generateContent:
curl --fail-with-body --max-time 620 \ -X POST "https://api.rendergrid.io/ v1beta/ models/ gemini-3-pro-image:generateContent" \ -H "x-goog-api-key: $RENDERGRID_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "contents": [{"parts": [{"text": "A ceramic mug on marble, soft window light"}]}], "generationConfig": { "responseModalities": ["IMAGE"], "imageConfig": {"aspectRatio": "1:1", "imageSize": "1K"} } }' \ -o response.json
import os from google import genai from google.genai import types client = genai.Client( api_key=os.environ["RENDERGRID_API_KEY"], http_options=types.HttpOptions( base_url="https://api.rendergrid.io", timeout=620_000, ), ) response = client.models.generate_content( model="gemini-3-pro-image", contents="A ceramic mug on marble, soft window light", config=types.GenerateContentConfig( response_modalities=["IMAGE"], image_config=types.ImageConfig(aspect_ratio="1:1", image_size="1K"), ), ) for part in response.parts: if part.inline_data: extension = part.inline_data.mime_type.removeprefix("image/") with open("result." + extension, "wb") as file: file.write(part.inline_data.data)
import { writeFileSync } from "node:fs"; import { GoogleGenAI } from "@google/genai"; const ai = new GoogleGenAI({ apiKey: process.env.RENDERGRID_API_KEY, httpOptions: { baseUrl: "https://api.rendergrid.io", timeout: 620_000 }, }); const response = await ai.models.generateContent({ model: "gemini-3-pro-image", contents: "A ceramic mug on marble, soft window light", config: { responseModalities: ["IMAGE"], imageConfig: { aspectRatio: "1:1", imageSize: "1K" }, }, }); for (const part of response.candidates[0].content.parts) { if (part.inlineData) { const extension = part.inlineData.mimeType.replace("image/", ""); writeFileSync(`result.${extension}`, Buffer.from(part.inlineData.data, "base64")); } }
In Python we recommend generate_content, without a stream, for long generations.
A stream — streamGenerateContent with the alt=sse parameter (curl and Node.js):
curl --fail-with-body --no-buffer --max-time 620 \ -X POST "https://api.rendergrid.io/ v1beta/ models/ gemini-3-pro-image:streamGenerateContent?alt=sse" \ -H "x-goog-api-key: $RENDERGRID_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "contents": [{"parts": [{"text": "A ceramic mug on marble, soft window light"}]}], "generationConfig": { "responseModalities": ["IMAGE"], "imageConfig": {"aspectRatio": "1:1", "imageSize": "1K"} } }'
import { writeFileSync } from "node:fs"; import { GoogleGenAI } from "@google/genai"; const ai = new GoogleGenAI({ apiKey: process.env.RENDERGRID_API_KEY, httpOptions: { baseUrl: "https://api.rendergrid.io", timeout: 620_000 }, }); const stream = await ai.models.generateContentStream({ model: "gemini-3-pro-image", contents: "A ceramic mug on marble, soft window light", config: { responseModalities: ["IMAGE"], imageConfig: { aspectRatio: "1:1", imageSize: "1K" }, }, }); for await (const chunk of stream) { for (const part of chunk.candidates?.[0]?.content?.parts ?? []) { if (part.inlineData) { const extension = part.inlineData.mimeType.replace("image/", ""); writeFileSync(`result.${extension}`, Buffer.from(part.inlineData.data, "base64")); } } }
The client timeout in the examples is 620 seconds, by the rule in the “Waiting for the response” section.
The other routes. The list of models and one model:
curl --fail-with-body --max-time 30 \ "https://api.rendergrid.io/ v1beta/ models" \ -H "x-goog-api-key: $RENDERGRID_API_KEY"
curl --fail-with-body --max-time 30 \ "https://api.rendergrid.io/ v1beta/ models/ gemini-3-pro-image" \ -H "x-goog-api-key: $RENDERGRID_API_KEY"
Under /v1 the request body is in the file request.json: it is the body from the first example.
curl --fail-with-body --max-time 620 \ -X POST "https://api.rendergrid.io/ v1/ models/ gemini-3-pro-image:generateContent" \ -H "x-goog-api-key: $RENDERGRID_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json \ -o response.json
curl --fail-with-body --no-buffer --max-time 620 \ -X POST "https://api.rendergrid.io/ v1/ models/ gemini-3-pro-image:streamGenerateContent?alt=sse" \ -H "x-goog-api-key: $RENDERGRID_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json
Models
You can call the identifiers from the table. Google’s identifiers and ours lead to the same Rendergrid model; the price and the balance are those of that model in the regular API.
| Identifier in the request | Rendergrid model |
|---|---|
gemini-3-pro-image | nano-banana-pro |
gemini-3.1-flash-image | nano-banana-2 |
gemini-2.5-flash-image | nano-banana-flash |
gemini-3-pro-image-preview | nano-banana-pro |
gemini-3.1-flash-image-preview | nano-banana-2 |
nano-banana-pro | nano-banana-pro |
nano-banana-2 | nano-banana-2 |
nano-banana-2.1 | nano-banana-2.1 |
nano-banana-flash | nano-banana-flash |
nano-banana-2-lite | nano-banana-2-lite |
GET /v1beta/ shows only what your key can call: first Google’s identifier, then ours. Variants with -preview are accepted but are not in the list.
If the account has no right to a model, the response is 403. An unknown or not opened model is 404. There are no other models on this path: GPT models answer 404.
Request
The request body is Google’s generateContent body. What is taken from it:
- Parts. A part is
text,inlineData(mimeTypeanddatain base64) orfileData(fileUriand, optionally,mimeType). AfileUriaddress is http or https only; it is checked in the same way as reference image addresses in the regular API (see “Reference images”). - Prompt text. It is the text parts of the last user turn, joined by a line break; the text of
systemInstructiongoes before them, separated by an empty line. - Reference images. The reference images are all the pictures of all turns, the user’s and the model’s, in order. Inline images are PNG, JPEG and WebP only; each is uploaded once and passed on by link, like any upload. Inline images and
fileUricannot be sent together in one request: the response is400. - Frame shape.
generationConfig.imageConfig.aspectRatio. The allowed values are the same as foraspect_ratioin the regular API (see “Models and parameters”). Without it:autoif the request has reference images, and1:1if it has none;nano-banana-2-litehas noautomode, and its frame comes out1:1. - Size.
generationConfig.imageConfig.imageSize:1K,2Kor4K. Without it:1K, not2Kas in the regular API. A size the model does not have (see the table in the “Response” section) is rejected when the request is accepted, as in the regular API:400. - File format.
generationConfig.imageConfig.imageOutputOptions.mimeType:image/pngorimage/jpeg. Without it the file comes in the model’s own format, without re-encoding. Any other value is400. - Variants and modalities.
responseModalitiesmust containIMAGE(TEXTalone is400);candidateCountcan only be 1.
{
"contents": [
{
"role": "user",
"parts": [
{ "text": "Put this mug on a marble table, soft window light" },
{ "fileData": { "mimeType": "image/png", "fileUri": "https:// example.com/ mug.png" } }
]
}
],
"generationConfig": {
"responseModalities": ["IMAGE"],
"imageConfig": { "aspectRatio": "4:5", "imageSize": "2K" }
}
}These fields are accepted and ignored: tools, safetySettings, thinkingConfig, temperature, topP, topK, seed, thoughtSignature, cachedContent, labels. They do not affect the result and cause no errors.
Limits. The prompt text is at most 20,000 characters; reference images, at most 14; an inline image, at most 25 MiB after decoding, like a file upload (see “Reference images”); the request body, at most 64 MiB. Going over is 400 INVALID_ARGUMENT, and for the body 413 INVALID_ARGUMENT. A body of that size may be stopped by the entry server before the request is accepted: then the response is its own page, not a Google envelope.
Response
A successful response is Google’s GenerateContentResponse: the image is in candidates[0].content.parts[0].inlineData, and finishReason is STOP. The image type, mimeType, is taken from the bytes of the file itself.
{
"candidates": [
{
"content": {
"role": "model",
"parts": [{ "inlineData": { "mimeType": "image/png", "data": "iVBORw0KGgo…" } }]
},
"finishReason": "STOP",
"index": 0
}
],
"usageMetadata": {
"promptTokenCount": 11,
"candidatesTokenCount": 1120,
"totalTokenCount": 1131,
"candidatesTokensDetails": [{ "modality": "IMAGE", "tokenCount": 1120 }]
},
"modelVersion": "gemini-3-pro-image",
"responseId": "5b0214ae-9e39-460a-820a-f6ae3e24400b"
}modelVersion is the model identifier from your request, with -preview if you gave it. responseId is the request id, the same as in the x-rendergrid-request-id header. If accepting the request did not finish within 90 seconds and the 200 response went out before the id was there, the header is absent, and the id is only in responseId.
usageMetadata: promptTokenCount is an estimate, not a count of the model’s tokens: the length of the request in characters divided by 4, rounded up. candidatesTokenCount and totalTokenCount are counted by the table below; candidatesTokensDetails carries the same image tokens. Gemini clients need the tokens for their own accounting; the price of a request does not depend on them. A dash in the table means the model has no such size.
| Model | 1K | 2K | 4K |
|---|---|---|---|
nano-banana-pro | 1120 | 1120 | 2000 |
nano-banana-2 | 1120 | 1680 | 2520 |
nano-banana-2.1 | 1120 | 1680 | 2520 |
nano-banana-flash | 1290 | — | — |
nano-banana-2-lite | 1120 | — | — |
Waiting for the response
An image is not created instantly, and the connection has to be kept open. The timings:
| What | Value |
|---|---|
| The server is silent until the first byte | 90 seconds |
| The “alive” signal after that | every 15 seconds |
| The wait limit | 600 seconds from the moment the request is accepted |
- For the first 90 seconds the server sends nothing: a response that is ready earlier arrives with its own HTTP code, an error too.
- If there is no image by the 90th second, the server answers
200and sends a keep-alive every 15 seconds — a sign that the request is alive — and then the response. - After such a
200an error arrives in the body: as a Google error object, in SSE as adata:event. - The limit is 600 seconds from the moment the request is accepted: then
504 DEADLINE_EXCEEDEDarrives in the body with the textGeneration timed out., and the amount is returned. This path never answers with HTTP 502 or 504; a generation that ran out of time before the200answers503 UNAVAILABLE. - Set the read timeout in your client to more than 90 seconds, and the total one to at least 610: the server’s limit is 600 seconds, and the error response for the limit has to have time to arrive. A request dropped by the client is paid for.
Money
- The amount is charged when the request is accepted, before the work starts — as for an identical request to the regular API, and at the same price.
- An image is paid for as soon as it is created — as with Google.
- If the connection breaks on your side — while the request was waiting or while the response was being sent — the charge stays: the image is created to the end, and it is available by the request id (
GET /api/) and in the account history.public/ v1/ creations/ {id} - If the image was not delivered through our fault — the 600-second limit expired, the response broke on our side, or the server was shutting down — the amount is returned to the balance automatically, once.
- A failed request and a moderation refusal are free, as in the regular API: the amount is returned to the balance.
- A repeated request is a new request and a new charge: there is no idempotency key on this path.
- If the balance is not enough, the response is
429 RESOURCE_EXHAUSTED(in the regular API,402).
Errors
An error arrives in Google’s envelope: error.code, error.message and error.status.
{
"error": {
"code": 404,
"message": "The requested model was not found.",
"status": "NOT_FOUND"
}
}| HTTP code | Google status | When |
|---|---|---|
400 | INVALID_ARGUMENT | The body, the fields or the reference images failed the checks (see “Request”), or the request was rejected when it was accepted, as in the regular API. |
401 | UNAUTHENTICATED | There is no key or it is invalid; the text is as in the regular API. |
403 | PERMISSION_DENIED | The account is blocked, the key has no right to this model, or the path is closed for the account. |
404 | NOT_FOUND | An unknown model or path; there are no GPT models on this path. |
413 | INVALID_ARGUMENT | The request body is larger than 64 MiB. |
429 | RESOURCE_EXHAUSTED | The rate limit, the limit of jobs in progress, a block after access errors, or not enough money on the balance; the text of the response says which. The pause before a retry is in the Retry-After header. |
500 | INTERNAL | An internal error or a failed generation; the money for a failure is returned. |
503 | UNAVAILABLE | The service is temporarily unavailable, the generation did not finish in time before the 200 response, or the inline images did not manage to upload. In the last case Retry-After: 5 arrives, and no money is charged. |
504 | DEADLINE_EXCEEDED | Only in the response body: after the 200, when the 600-second limit is reached. The amount is returned. |
This path does not send 502 and 504 as an HTTP code. The 600-second limit arrives as 504 inside the body of a response that has already started with 200:
{
"error": {
"code": 504,
"message": "Generation timed out.",
"status": "DEADLINE_EXCEEDED"
}
}The texts of the errors given when a request is accepted (401, 429, validation refusals) are as in the regular API: see “Error codes”.
Moderation refusals
A moderation refusal arrives as 200, as with Google:
finishReason: "IMAGE_SAFETY"and no parts at all — the model refused on the result;promptFeedback.blockReason: "SAFETY"— a refusal on the request or on a reference image;finishReason: "NO_IMAGE"— there is no image.
{
"candidates": [{ "finishReason": "IMAGE_SAFETY", "index": 0 }],
"usageMetadata": { "promptTokenCount": 11, "totalTokenCount": 11 },
"modelVersion": "gemini-3-pro-image",
"responseId": "5b0214ae-9e39-460a-820a-f6ae3e24400b"
}Such responses are free, as in the regular API: the amount is returned to the balance.
Limits
The limits of the regular API apply: the request rate, the burst protection and the number of jobs in progress (see “Limits”). Each inline image is a separate upload: one unit of the rate and one unit of the upload limit. A place in the jobs limit is held while the generation runs, not while the connection is open.