# GPT Image, Nano Banana, Seedream API — one endpoint, async jobs

> Call GPT Image 2.5, Nano Banana Pro, Seedream 5.0 Pro and Flux 3 through one eroq endpoint: request, 202 jobs, polling, batches and errors, in curl and JS.

Published 2026-10-08 · eroq.ai — canonical: https://eroq.ai/blog/gpt-image-nano-banana-seedream-api


Five image makers usually means five SDKs, five keys, five billing dashboards and five ideas of what "async" means. On eroq it is one endpoint where `model` is the only thing that changes. OpenAI's GPT Image 2.5, Google's Nano Banana, ByteDance's Seedream, Black Forest Labs' Flux 3 and xAI's [Grok Imagine 2.0](/models/grok-imagine-2-0) all go through `POST /v1/images/generations`, with the same body, the same asynchronous contract and a flat credit price per take. It is the same engine, price and policy as the studio. The reference lives at [/docs/images](/docs/images); this is the practical version.

## The model ids

| Model | `model` id | Maker | Credits per take | Typical render |
|---|---|---|---|---|
| [GPT Image 2.5 Sunburst](/models/gpt-image-2-5-sunburst) | `gpt-image-2-5-sunburst` | OpenAI | 31 | about 15 s |
| [GPT Image 2.5 Flare](/models/gpt-image-2-5-flare) | `gpt-image-2-5-flare` | OpenAI | 31 | about 15 s |
| [Nano Banana Pro](/models/nano-banana-pro) | `nano-banana-pro` | Google | 124 | about 25 s |
| [Nano Banana 2.1](/models/nano-banana-2-1) | `nano-banana-2-1` | Google | 31 | about 8 s |
| [Seedream 5.0 Pro](/models/seedream-5-0-pro) | `seedream-5-0-pro` | ByteDance | 36 | about 20 s |
| [Flux 3 Image](/models/flux-3-image) | `flux-3-image` | Black Forest Labs | 32 | about 20 s |
| [Grok Imagine 2.0](/models/grok-imagine-2-0) | `grok-imagine-2-0` | xAI | 32 | about 15 s |

Every one is open to every account, pay-as-you-go included, at the same price for all five formats.

## Submit a render

```bash
curl https://eroq.ai/v1/images/generations \
  -H "Authorization: Bearer $EROQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "nano-banana-pro",
    "prompt": "An aerial view of a harbour city at blue hour, lit windows and light trails along the quays, layered haze between the towers, crisp architectural detail, cinematic wide shot",
    "aspect": "16:9",
    "batch": 2
  }'
```

The call charges the batch and answers `202` at once, with one job per take:

```json
{
  "object": "image.generation",
  "jobs": [
    { "id": "b7e6c2d4-…", "poll": "/v1/images/generations/b7e6c2d4-…" },
    { "id": "4f19a0e3-…", "poll": "/v1/images/generations/4f19a0e3-…" }
  ],
  "created": 1791450000,
  "model": "nano-banana-pro",
  "failed": 0,
  "usage": { "credits_spent": 248, "credits_remaining": 752 }
}
```

The fields that matter on these engines:

- **`model`**: always send it. Omitted, it defaults to `eroq-uncensored`; an unknown id answers `400 invalid_body` with the list of valid ones.
- **`prompt`**: the whole brief. Put lettering in double quotes.
- **`aspect`**: `1:1` (the default), `3:4`, `4:3`, `9:16` or `16:9`, rendered natively.
- **`batch`**: 1 to 4 takes, charged upfront. `failed` counts takes that could not start, already refunded.
- **`store`**: `true` for a permanent public URL, at 2 credits per started 10 MB on top.
- **`folder_id`**: drops the render straight into one of your Library folders.

## What these engines ignore

The engines above are text-to-image only, and a few fields do nothing on them:

- **`references`** are dropped. For a face or product that must match photos, send the request to `eroq-one` (up to ten pictures) or `eroq-krea2` (up to four).
- **`negative_prompt` and `cfg_scale`** pass validation but are not used by these engines. Describe the result you want in the prompt.
- **`edit: true`** answers `400 edit_unsupported`, and **`aspect: "retain"`** answers `400 retain_requires_reference`. Both are picture operations; route them to [Eroq One](/models/eroq-one).

## Poll until the job is terminal

```js
const BASE = 'https://eroq.ai/v1'
const headers = {
  Authorization: `Bearer ${process.env.EROQ_API_KEY}`,
  'Content-Type': 'application/json',
}
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))

export async function generate(body) {
  const res = await fetch(`${BASE}/images/generations`, { method: 'POST', headers, body: JSON.stringify(body) })
  const json = await res.json()
  if (!res.ok) throw Object.assign(new Error(json.error.message), { status: res.status, code: json.error.code })
  // One job per take: each succeeds, or fails and refunds, on its own
  return Promise.all(json.jobs.map(job => waitFor(job.id)))
}

async function waitFor(id, { every = 2500, timeout = 11 * 60_000 } = {}) {
  const started = Date.now()
  while (Date.now() - started < timeout) {
    const res = await fetch(`${BASE}/images/generations/${id}`, { headers })
    const job = await res.json()
    if (!res.ok) return { id, error: job.error?.code ?? `http_${res.status}` }
    if (job.status === 'succeeded') {
      const [image] = job.data
      return { id, url: image.url, libraryId: image.library_id, ms: job.generation_ms }
    }
    if (job.status === 'failed') return { id, error: job.error.code } // credits already back
    await sleep(every) // processing: phase is "queued" or "generating"
  }
  return { id, error: 'client_timeout' }
}
```

Polls are free, and every few seconds is the intended cadence. Keep going until `status` is `succeeded` or `failed`. The `image.generation.succeeded` and `image.generation.failed` webhooks are a notification on top, not a replacement; [the async API guide](/blog/async-video-generation-api-webhooks) shows how to verify their signature.

On success, `data[0].url` is a signed URL that lasts 30 minutes. Job ids live 24 hours, and re-fetching the job (or the creation, by `library_id`) mints a fresh URL. Send `store: true` when your users need an address that never changes. `generation_ms` is the wall-clock time of the render, and a job still processing past the 10-minute server deadline is failed with `generation_timeout` and refunded, which is why the client timeout above sits just past it.

## Batches, briefly

A batch is one call, one prompt and up to four takes. Each take is its own job, so poll them in parallel and keep whichever comes back best; a take that fails refunds itself without touching the others. A batch also counts as a single call against the per-minute rate limit. For four different prompts, send four calls.

## Errors

| Status | `code` | What happened | What to do |
|---|---|---|---|
| 400 | `invalid_body` | A field failed validation | Fix the body; the message names the field |
| 400 | `content_blocked` | Refused by the acceptable-use policy, before any charge | Change the prompt, do not retry as is |
| 400 | `edit_unsupported` | A picture operation sent to a text-only engine | Use `eroq-one` |
| 402 | `insufficient_credits` | Balance below price × batch | Top up; never retry in a loop |
| 429 | `rate_limit_exceeded` | Too many calls this minute on this key | Wait for `Retry-After` |
| 502 | `generation_failed` | The render could not start | Not charged; retry |
| 503 | `model_unavailable` | The engine is offline on this deployment | Not charged; pick another id |
| 404 | `not_found` | Unknown job id, or older than 24 hours | Store ids and images promptly |

A job that ends `failed` carries `error.code` `content_blocked` (the provider refused it), `generation_failed` or `generation_timeout`, and its credits are already back in the wallet. Plans raise the per-minute cap on every key: 2× on Creator, 4× on Studio.

## Route by job

```js
const ENGINE = {
  text: 'gpt-image-2-5-sunburst', // posters, packaging, UI mockups
  keyArt: 'gpt-image-2-5-flare', // dramatic, cinematic frames
  detail: 'seedream-5-0-pro', // texture, architecture, landscapes
  hero: 'nano-banana-pro', // the final frame, top fidelity
  draft: 'nano-banana-2-1', // fast iteration
  explore: 'grok-imagine-2-0', // many directions, characterful
  daylight: 'flux-3-image', // natural light and typography
}
```

`model: "auto"` lets eroq's router pick among the engines your plan unlocks and bills the one it chose, reported back in `model`. That can be Nano Banana Pro at 124 credits / image, so name the model whenever cost matters. The per-take arithmetic is in [what AI images cost in 2026](/blog/ai-image-generation-cost-2026), and [the best AI image models of 2026](/blog/best-ai-image-models-2026) explains the routing choices.

## MCP and CLI

The same models sit behind the MCP server's `generate_image` tool, which waits for the render and returns the picture (with `get_image_status` for slow ones), and behind the CLI: `eroq image "a foggy harbour at dawn" -m seedream-5-0-pro --aspect 16:9 -o harbour.webp`. Setup for both is in [/docs/mcp](/docs/mcp).

## FAQ

### Do I need OpenAI, Google or ByteDance API keys?

No. One eroq key covers every model, and everything bills in eroq credits from one wallet.

### Can I send a reference image to Nano Banana Pro or GPT Image 2.5 through eroq?

No. On eroq they are text-to-image engines and references are dropped. For picture-driven work, use `eroq-one`, which reads up to ten references and edits by instruction.

### How long does the image URL last?

The signed URL in `data` lasts 30 minutes and the job 24 hours. Re-fetch for a fresh URL, or submit with `store: true` for a permanent public one.

### Do failed or blocked renders cost credits?

No. A prompt refused by policy is answered before any charge, and a job that fails or is refused upstream refunds itself.

Grab a key, pick a model id and send one request to [/docs/images](/docs/images).
