블로그/developers·2026년 10월 8일·6분·eroq 팀 작성
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.
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 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; this is the practical version.
The model ids
| Model | model id |
Maker | Credits per take | Typical render |
|---|---|---|---|---|
| GPT Image 2.5 Sunburst | gpt-image-2-5-sunburst |
OpenAI | 31 | about 15 s |
| GPT Image 2.5 Flare | gpt-image-2-5-flare |
OpenAI | 31 | about 15 s |
| Nano Banana Pro | nano-banana-pro |
124 | about 25 s | |
| Nano Banana 2.1 | nano-banana-2-1 |
31 | about 8 s | |
| Seedream 5.0 Pro | seedream-5-0-pro |
ByteDance | 36 | about 20 s |
| Flux 3 Image | flux-3-image |
Black Forest Labs | 32 | about 20 s |
| 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
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:
{
"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 toeroq-uncensored; an unknown id answers400 invalid_bodywith the list of valid ones.prompt: the whole brief. Put lettering in double quotes.aspect:1:1(the default),3:4,4:3,9:16or16:9, rendered natively.batch: 1 to 4 takes, charged upfront.failedcounts takes that could not start, already refunded.store:truefor 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:
referencesare dropped. For a face or product that must match photos, send the request toeroq-one(up to ten pictures) oreroq-krea2(up to four).negative_promptandcfg_scalepass validation but are not used by these engines. Describe the result you want in the prompt.edit: trueanswers400 edit_unsupported, andaspect: "retain"answers400 retain_requires_reference. Both are picture operations; route them to Eroq One.
Poll until the job is terminal
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 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
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, and the best AI image models of 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.
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.
이 글에 나온 모델로 직접 만들어 보세요 — 무료 크레딧 50개로 시작하거나, 모든 엔진과 가격도 살펴보세요.