Image Generation
Generate AI images from text prompts and reference images, synchronously or as async tasks, with credit estimation.
The Image Generation API turns a text prompt (optionally guided by reference images) into one or more images. You can call it three ways:
- Synchronous —
POST /v1/images/generationblocks until the image is ready and returns the raw model output (base64 image data) in the response body. - Asynchronous —
POST /v1/images/generation/asyncreturns ataskIdimmediately; pollGET /v1/images/generation/tasks/{taskId}for the result (hosted image URLs). - Estimate first —
POST /v1/images/generation/estimate-creditsreturns the credit cost and whether your account can generate, without spending anything.
All endpoints require an API key (Authorization: Bearer $LISTENHUB_API_KEY). Create keys at
listenhub.ai/settings/api-keys.
The two generation endpoints return data differently. The synchronous endpoint returns the
raw model JSON directly in the body (not wrapped in the standard { code, message, data }
envelope). The async and estimate endpoints use the standard wrapped envelope. See
Response Formats.
Generate Image (synchronous)
POST /v1/images/generation
Generate an image from a text prompt and block until it is ready. Optionally supply reference images to guide the style or content. The response body is the raw model output — JSON containing base64 image data.
Basic generation
curl -X POST "https://api.marswave.ai/openapi/v1/images/generation" \
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"provider": "google",
"model": "gemini-3-pro-image",
"prompt": "A serene mountain landscape at sunset with a reflective lake",
"imageConfig": {
"aspectRatio": "16:9",
"imageSize": "2K"
}
}'const response = await fetch(
'https://api.marswave.ai/openapi/v1/images/generation',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LISTENHUB_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
provider: 'google',
model: 'gemini-3-pro-image',
prompt: 'A serene mountain landscape at sunset with a reflective lake',
imageConfig: {
aspectRatio: '16:9',
imageSize: '2K',
},
}),
},
)
const data = await response.json()
// data.candidates[0].content.parts[0].inlineData holds the generated imageimport os
import requests
response = requests.post(
'https://api.marswave.ai/openapi/v1/images/generation',
headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
json={
'provider': 'google',
'model': 'gemini-3-pro-image',
'prompt': 'A serene mountain landscape at sunset with a reflective lake',
'imageConfig': {
'aspectRatio': '16:9',
'imageSize': '2K',
},
},
)
data = response.json()
# data['candidates'][0]['content']['parts'][0]['inlineData'] holds the generated imageTo use GPT-Image-2, set provider to "openai" and model to "gpt-image-2". The request and
response format is the same — only provider, model, and the imageConfig differ. See the
Provider and Model Matrix.
Generation with reference images
Supply reference images to guide the output. Each reference image is either a URL (fileData) or
base64-encoded inline data (inlineData). You can mix both formats in one request.
Using image URLs
curl -X POST "https://api.marswave.ai/openapi/v1/images/generation" \
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"provider": "google",
"model": "gemini-3-pro-image",
"prompt": "Transform this scene into a watercolor painting style",
"referenceImages": [
{
"fileData": {
"fileUri": "https://example.com/my-photo.jpg",
"mimeType": "image/jpeg"
}
}
],
"imageConfig": {
"aspectRatio": "1:1",
"imageSize": "2K"
}
}'const response = await fetch(
'https://api.marswave.ai/openapi/v1/images/generation',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LISTENHUB_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
provider: 'google',
model: 'gemini-3-pro-image',
prompt: 'Transform this scene into a watercolor painting style',
referenceImages: [
{
fileData: {
fileUri: 'https://example.com/my-photo.jpg',
mimeType: 'image/jpeg',
},
},
],
imageConfig: {
aspectRatio: '1:1',
imageSize: '2K',
},
}),
},
)
const data = await response.json()import os
import requests
response = requests.post(
'https://api.marswave.ai/openapi/v1/images/generation',
headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
json={
'provider': 'google',
'model': 'gemini-3-pro-image',
'prompt': 'Transform this scene into a watercolor painting style',
'referenceImages': [
{
'fileData': {
'fileUri': 'https://example.com/my-photo.jpg',
'mimeType': 'image/jpeg',
}
}
],
'imageConfig': {
'aspectRatio': '1:1',
'imageSize': '2K',
},
},
)
data = response.json()Using base64 inline data
curl -X POST "https://api.marswave.ai/openapi/v1/images/generation" \
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"provider": "google",
"model": "gemini-3-pro-image",
"prompt": "Create a cartoon version of this portrait",
"referenceImages": [
{
"inlineData": {
"data": "<BASE64_ENCODED_IMAGE>",
"mimeType": "image/png"
}
}
]
}'import { readFileSync } from 'fs'
const imageBase64 = readFileSync('reference.png').toString('base64')
const response = await fetch(
'https://api.marswave.ai/openapi/v1/images/generation',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LISTENHUB_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
provider: 'google',
model: 'gemini-3-pro-image',
prompt: 'Create a cartoon version of this portrait',
referenceImages: [
{
inlineData: {
data: imageBase64,
mimeType: 'image/png',
},
},
],
}),
},
)
const data = await response.json()import os
import base64
import requests
with open('reference.png', 'rb') as f:
image_base64 = base64.b64encode(f.read()).decode('utf-8')
response = requests.post(
'https://api.marswave.ai/openapi/v1/images/generation',
headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
json={
'provider': 'google',
'model': 'gemini-3-pro-image',
'prompt': 'Create a cartoon version of this portrait',
'referenceImages': [
{
'inlineData': {
'data': image_base64,
'mimeType': 'image/png',
}
}
],
},
)
data = response.json()Request parameters
These parameters apply to POST /v1/images/generation, POST /v1/images/generation/async, and
POST /v1/images/generation/estimate-credits — the three endpoints share one request schema.
| Field | Type | Required | Description |
|---|---|---|---|
provider | string | Yes¹ | Model provider: google, openai, or bytedance |
model | string | No | Model name. Defaults to gpt-image-2. See Provider and Model Matrix |
prompt | string | Yes² | Text description of the image to generate |
referenceImages | array | No | Reference images to guide generation. See Reference image limits |
referenceImages[].fileData | object | No | Reference image supplied as a URL |
referenceImages[].fileData.fileUri | string | Yes | Image URL — scheme must be http, https, or gs |
referenceImages[].fileData.mimeType | string | Yes | MIME type: image/png, image/jpeg, image/webp, image/heic, or image/heif |
referenceImages[].inlineData | object | No | Reference image supplied as base64-encoded data |
referenceImages[].inlineData.data | string | Yes | Base64-encoded image data |
referenceImages[].inlineData.mimeType | string | Yes | MIME type: image/png, image/jpeg, image/webp, image/heic, or image/heif |
imageConfig | object | No | Image output configuration. Defaults to { "imageSize": "2K" } |
imageConfig.imageSize | string | No | Output resolution: 1K, 2K (default), or 4K |
imageConfig.aspectRatio | string | No | Aspect ratio. Defaults to 1:1. See Aspect ratios |
imageConfig.quality | string | No | Render quality: low, medium, or high. Applies to GPT-Image-2; omit to let the model decide |
¹ provider is required on the two generation endpoints and optional on estimate-credits.
² prompt is required on the two generation endpoints; on estimate-credits it may be empty or
omitted (it only affects the input-token estimate).
Each item in referenceImages must contain exactly one of fileData or inlineData, not both.
Provider and Model Matrix
provider selects the vendor; model selects the specific model. The default model is
gpt-image-2.
provider | model | Notes |
|---|---|---|
google | gemini-3-pro-image | Higher quality, more detailed output. NanoBanana Pro |
google | gemini-3.1-flash-image | Faster generation. Supports the extra 1:4 / 4:1 / 1:8 / 8:1 ratios |
openai | gpt-image-2 | Strong prompt following. At most 4 reference images. aspectRatio optional |
bytedance | seedream-5-0-pro | Precise editing (coordinates / color codes go in prompt). Up to 10 reference images. 1K / 2K only |
Legacy preview model IDs gemini-3-pro-image-preview and gemini-3.1-flash-image-preview are
still accepted as input and normalized to their GA IDs above. Send the GA IDs for new
integrations.
Aspect ratios
imageConfig.aspectRatio defaults to 1:1. The following ratios are accepted by the schema:
| Ratio | Description |
|---|---|
1:1 | Square |
2:3 | Portrait |
3:2 | Landscape |
3:4 | Portrait |
4:3 | Landscape |
9:16 | Vertical / mobile |
16:9 | Widescreen |
21:9 | Ultra-wide |
1:4 | Flash only |
4:1 | Flash only |
1:8 | Flash only |
8:1 | Flash only |
1:4, 4:1, 1:8, and 8:1 are accepted only by gemini-3.1-flash-image and seedream-5-0-pro. GPT-Image-2 supports
the eight standard ratios (1:1, 2:3, 3:2, 3:4, 4:3, 9:16, 16:9, 21:9) and lets you
omit aspectRatio to choose automatically. A ratio unsupported by the selected model returns
400.
Image sizes
imageConfig.imageSize accepts 1K, 2K (default), and 4K. Larger sizes cost more credits and,
for GPT-Image-2, 4K (and high quality) requires an active paid subscription. Seedream 5.0 Pro
supports 1K and 2K only; 4K returns 400. Do not hardcode
credit costs — call estimate-credits for the exact figure.
Reference image limits
| Model | Max reference images |
|---|---|
gemini-3-pro-image | 14 |
gemini-3.1-flash-image | 14 |
gpt-image-2 | 4 |
seedream-5-0-pro | 10 |
The schema caps referenceImages at 14 items overall. GPT-Image-2 enforces a tighter limit of 4 and
Seedream 5.0 Pro a limit of 10 — exceeding it returns 400. Accepted MIME types for both fileData and inlineData are
image/png, image/jpeg, image/webp, image/heic, and image/heif.
Seedream 5.0 Pro precise editing
Seedream 5.0 Pro supports precise editing: changing a specific region of an image instead of regenerating the whole thing.
There is no separate edit endpoint and no mask or region parameter — editing uses the same
generation endpoint, with the source image in referenceImages and the "what to change, and how"
written into prompt. The model reads absolute pixel coordinates (origin at the top-left corner of
the image) and industry color codes directly.
{
"provider": "bytedance",
"model": "seedream-5-0-pro",
"prompt": "Treating the top-left corner as the coordinate origin, change the content inside top-left:(376,363) bottom-right:(701,638) to green, and leave everything else unchanged",
"referenceImages": [
{
"fileData": {
"fileUri": "https://assets.listenhub.ai/your-source-image.png",
"mimeType": "image/png"
}
}
],
"imageConfig": { "imageSize": "2K", "aspectRatio": "1:1" }
}If your product offers visual editing gestures (box select, lasso, arrows), translate them into
coordinate strings in your own frontend — or bake the annotations into the reference image — before
writing them into prompt. The server passes the prompt through verbatim and does not parse
coordinates. Keep aspectRatio matching the source image's real ratio; a mismatch can make the
model reconstruct the whole image instead of editing one region.
Estimate credits
POST /v1/images/generation/estimate-credits
Returns the credit cost for a given configuration and whether your account can generate it, without
spending credits or calling the model. Use it to show a price before committing, or to check whether
a 4K / high request needs a subscription. Accepts the same body as the generation endpoints;
provider and prompt are optional here.
curl -X POST "https://api.marswave.ai/openapi/v1/images/generation/estimate-credits" \
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-image-2",
"imageConfig": {
"imageSize": "2K",
"aspectRatio": "1:1",
"quality": "medium"
}
}'const response = await fetch(
'https://api.marswave.ai/openapi/v1/images/generation/estimate-credits',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LISTENHUB_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-image-2',
imageConfig: { imageSize: '2K', aspectRatio: '1:1', quality: 'medium' },
}),
},
)
const { data } = await response.json()
console.log(data.credits, data.canGenerate)import os
import requests
response = requests.post(
'https://api.marswave.ai/openapi/v1/images/generation/estimate-credits',
headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
json={
'model': 'gpt-image-2',
'imageConfig': {'imageSize': '2K', 'aspectRatio': '1:1', 'quality': 'medium'},
},
)
data = response.json()['data']
print(data['credits'], data['canGenerate'])Estimate response
Wrapped in the standard envelope. The data object:
| Field | Type | Description |
|---|---|---|
model | string | Normalized GA model ID used for the estimate |
imageSize | string | Resolved output size |
aspectRatio | string | Resolved aspect ratio (absent when the model auto-selects) |
quality | string | Resolved quality (absent when not applicable) |
pixels | object | { "width": number, "height": number, "size": "WxH" } when resolved |
credits | number | Credits this configuration would cost |
canGenerate | boolean | Whether the account has enough effective credit balance |
requiresSubscription | boolean | true when the config needs an active paid plan (e.g. 4K, high) |
pricing | object | Pricing metadata: pricingVersion, mode (token-estimate/fixed) |
warnings | array | Advisory strings, e.g. reference_image_input_tokens_not_included |
{
"code": 0,
"message": "",
"data": {
"model": "gpt-image-2",
"imageSize": "2K",
"aspectRatio": "1:1",
"quality": "medium",
"pixels": { "width": 2048, "height": 2048, "size": "2048x2048" },
"credits": 6,
"canGenerate": true,
"requiresSubscription": false,
"pricing": { "pricingVersion": "...", "mode": "token-estimate" },
"warnings": []
}
}Asynchronous generation
For long-running or high-resolution jobs, submit the task and poll for the result instead of holding a request open.
Create an async task
POST /v1/images/generation/async
Same request body as the synchronous endpoint. Returns 202 with a taskId immediately; generation
runs in the background and the resulting images are persisted as hosted URLs.
curl -X POST "https://api.marswave.ai/openapi/v1/images/generation/async" \
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"provider": "google",
"model": "gemini-3-pro-image",
"prompt": "An astronaut riding a horse on Mars, photorealistic",
"imageConfig": { "imageSize": "4K", "aspectRatio": "16:9" }
}'const res = await fetch(
'https://api.marswave.ai/openapi/v1/images/generation/async',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LISTENHUB_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
provider: 'google',
model: 'gemini-3-pro-image',
prompt: 'An astronaut riding a horse on Mars, photorealistic',
imageConfig: { imageSize: '4K', aspectRatio: '16:9' },
}),
},
)
const { data } = await res.json()
const taskId = data.taskIdimport os
import requests
res = requests.post(
'https://api.marswave.ai/openapi/v1/images/generation/async',
headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
json={
'provider': 'google',
'model': 'gemini-3-pro-image',
'prompt': 'An astronaut riding a horse on Mars, photorealistic',
'imageConfig': {'imageSize': '4K', 'aspectRatio': '16:9'},
},
)
task_id = res.json()['data']['taskId']Response (202):
{
"code": 0,
"message": "",
"data": { "taskId": "65f0...", "status": "pending" }
}Get a task
GET /v1/images/generation/tasks/{taskId}
Poll for the status and result of a single task. status is one of pending, generating,
success, or fail. On success, images holds the hosted result URLs.
curl "https://api.marswave.ai/openapi/v1/images/generation/tasks/65f0abc..." \
-H "Authorization: Bearer $LISTENHUB_API_KEY"{
"code": 0,
"message": "",
"data": {
"taskId": "65f0abc...",
"status": "success",
"images": [
{ "url": "https://.../0.png", "mimeType": "image/png" }
],
"createdAt": 1750000000000,
"completedAt": 1750000020000
}
}| Field | Type | Description |
|---|---|---|
taskId | string | Task identifier |
status | string | pending, generating, success, or fail |
images | array | Present on success; each item is { url, mimeType } |
failMsg | string | Failure message when status is fail |
createdAt | number | Creation time (epoch milliseconds) |
completedAt | number | Completion time (epoch milliseconds), when finished |
List tasks
GET /v1/images/generation/tasks
List your image tasks, newest first.
| Query param | Type | Default | Description |
|---|---|---|---|
page | number | 1 | Page number, minimum 1 |
pageSize | number | 20 | Items per page, 1–100 |
status | string | — | Filter by pending, generating, success, or fail |
curl "https://api.marswave.ai/openapi/v1/images/generation/tasks?page=1&pageSize=20&status=success" \
-H "Authorization: Bearer $LISTENHUB_API_KEY"{
"code": 0,
"message": "",
"data": {
"items": [
{ "taskId": "65f0...", "status": "success", "images": [/* ... */], "createdAt": 1750000000000, "completedAt": 1750000020000 }
],
"page": 1,
"pageSize": 20,
"total": 1
}
}Background tasks left in pending or generating for more than 30 minutes are swept to fail
with a timeout failMsg. Treat a non-terminal status older than that as failed and retry.
Response formats
| Endpoint | Wrapped envelope? | Body |
|---|---|---|
POST /v1/images/generation | No — raw model JSON | Base64 image data (see below) |
POST /v1/images/generation/async | Yes | { taskId, status } |
POST /v1/images/generation/estimate-credits | Yes | Estimate object |
GET /v1/images/generation/tasks | Yes | Paginated { items, page, pageSize, total } |
GET /v1/images/generation/tasks/{taskId} | Yes | Task object |
The synchronous endpoint returns the raw model output directly (not wrapped). A successful body contains the generated image as base64 data:
{
"candidates": [
{
"content": {
"parts": [
{
"inlineData": {
"mimeType": "image/png",
"data": "<BASE64_ENCODED_IMAGE>"
}
}
]
}
}
]
}Decode the data field from base64 to obtain the image file. The async path persists the image
for you and returns hosted urls on the task object — no base64 decoding needed.
NanoBanana Pro free quota
API key calls draw on the same account-level free-quota (freeUsages) balance as the web and
Labnana apps. You keep earning quota through sign-up, invites, and check-ins, then spend it
through the API. Query the live balance via
GET /v1/user/subscription and read its freeUsages
map.
When the matching balance is greater than 0, a 1K / 2K request spends one free generation
instead of credits. Once the balance hits 0, the same request falls back to normal credit billing.
Free quota applies only to 1K and 2K sizes. A 4K request never draws from freeUsages
and is always billed in credits.
How a NanoBanana Pro relax call runs depends on your account type:
- Paid or charged accounts (active subscription, recharge, or credit-pack purchase) get the normal paid generation experience even while spending free quota — full priority, normal capacity, normal fallback. The free quota only changes billing; it does not put you on a throttled lane.
- Pure-free accounts (never paid anything) run NanoBanana Pro relax on a lowest-priority free lane with a fixed throughput cap. At peak times a request may be queued or rejected with a retryable busy/timeout response. When that happens, no credits are spent and the free quota is not consumed — retry later (it is faster late at night).
A pure-free relax failure returns machine-readable metadata so you can detect it without parsing localized text:
failReasonisfree_relax_busyorfree_relax_timeout.retryableistrue.freeUsageRolledBackistrueonce the free quota has been refunded.userMessagecarries friendly, localizable copy.
For synchronous requests this appears in the error body; for async requests it appears on the failed task detail (and in the task list). Treat both reasons as "retry later, nothing was charged".
Rate limiting and reference image mode
Standard text-to-image requests are subject to per-user and global rate limits.
Reference image mode (referenceImages with inlineData) is subject to additional
server-side resource constraints. During peak periods, base64 requests may be throttled more
aggressively. On a 429, read the Retry-After header and back off before retrying. Implement
exponential backoff in your client.
Error codes
Errors use the standard envelope (code non-zero) or, for the synchronous endpoint, the raw error
body documented in NanoBanana Pro free quota.
| HTTP status | Meaning |
|---|---|
400 | Invalid request parameters (e.g. an aspect ratio unsupported by the selected model) |
402 | Insufficient credits |
429 | Rate limited or service busy — read Retry-After and retry |
500 | Image generation failed — retry the request |