Docs
Image generation
Two endpoints: text-to-image and image edit / image-to-image. Both return result URLs that stay live for ~24 hours — download what you want to keep.
Kunavo exposes /v1/images/generations (text-to-image) and /v1/images/edits (image-to-image / edit). Both are synchronous by default — the call blocks until the upstream completes, then returns result URLs — or go async with "async": true or a webhook_url.
Text-to-image
Endpoint: POST /v1/images/generations. Use the OpenAI SDK's images.generate method or curl directly.
img = client.images.generate(
model="nano-banana-pro",
prompt="a red origami crane on a white desk, soft window light",
size="1024x1024",
)
print(img.data[0].url)Common params
| Param | Type | Notes |
|---|---|---|
model | string | Any image model slug from /v1/models. |
prompt | string | Text prompt. Always required. |
size | string | OpenAI-style "1024x1024". Mapped to upstream aspect-ratio. |
quality | standard|hd | Where supported (gpt-image). |
resolution | 1K|2K|4K | Resolution tier for tiered models — see Resolution tiers below. Billed by tier. |
n | int | Number of images. Most models return 1 regardless. |
input | object | Escape hatch — see Raw input below. |
Resolution tiers
Several image models charge a stepped price by output resolution. Pass resolution to pick a tier; omit it and the default tier is used. You are billed for the tier actually produced.
| Model (+ its -edit variant) | Tiers — price each | Default |
|---|---|---|
| nano-banana-2 | 1K $0.0469 · 2K $0.0707 · 4K $0.1057 | 1K |
| nano-banana-pro | 1K/2K $0.0938 · 4K $0.168 | 2K |
| gpt-image-2 | 1K / 2K / 4K — $0.0886 (flat) | 1K |
The veo-3 family is tiered by resolution too — per-tier prices are on each model's /models page.
resolution = 4K with a square size, the call is automatically stepped down to — and billed at — 2K.Image edit / image-to-image
Endpoint: POST /v1/images/edits. Pass the source image as a URL (HTTPS or data: base64) via image for single-image edits, or image_urls for multi-image references.
# Image edit / image-to-image: pass the source image URL
import requests
resp = requests.post(
"https://api.kunavo.com/v1/images/edits",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"model": "gpt-image-2-edit",
"prompt": "recolor the crane to bright blue, keep composition",
"image": "https://your-cdn.com/source.png",
},
)
print(resp.json()["data"][0]["url"])Multi-image reference
Some models (Nano Banana Edit, GPT-Image-2 Edit) accept multiple reference images.
# nano-banana-edit accepts multiple reference images
resp = requests.post(
"https://api.kunavo.com/v1/images/edits",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"model": "nano-banana-edit",
"prompt": "merge subject from img1 with background from img2",
"image_urls": [
"https://your-cdn.com/subject.png",
"https://your-cdn.com/background.png",
],
},
)Supported edit models
nano-banana-edit— Google Nano Banana editgpt-image-2-edit— OpenAI image-to-image
Providing a source image
Image-edit and image-to-video both need a source image that the model can fetch. You have three options — pick whichever fits your app:
- You already have a public URL — pass it straight as
image(orimage_urlfor video). Done. - You have a local file — upload it once via
POST /v1/files, then reuse the returned permanent URL. - You have raw bytes in code — inline them as a
data:base64 URI; Kunavo hosts them transparently.
Option A — upload via POST /v1/files
Send multipart/form-data with a file field (or JSON { "file": "data:..." }). You get back a permanent files.kunavo.com URL — files up to 25 MB.
# Upload a local file → get a permanent Kunavo URL
import requests
with open("source.png", "rb") as f:
up = requests.post(
"https://api.kunavo.com/v1/files",
headers={"Authorization": f"Bearer {API_KEY}"},
files={"file": f},
).json()
# up["url"] is now a permanent https://files.kunavo.com/... URL
edit = requests.post(
"https://api.kunavo.com/v1/images/edits",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"model": "gpt-image-2-edit",
"prompt": "recolor the crane to bright blue",
"image": up["url"],
},
).json()
print(edit["data"][0]["url"])Option B — inline base64
Skip the separate upload entirely: put a data: URI in the image / image_url field. Kunavo decodes it, hosts it on its CDN, and feeds the hosted URL to the model.
# Or skip the upload step — pass base64 inline, we host it for you.
import base64, requests
with open("source.png", "rb") as f:
b64 = base64.b64encode(f.read()).decode()
resp = requests.post(
"https://api.kunavo.com/v1/images/edits",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"model": "gpt-image-2-edit",
"prompt": "recolor the crane to bright blue",
"image": f"data:image/png;base64,{b64}",
},
)POST /v1/video/generations — image_url accepts an https URL or a data: base64 URI for image-to-video models like Veo 3.Response shape
Both endpoints return OpenAI's standard images response:
{
"created": 1779360636,
"data": [
{ "url": "https://tempfile.aiquickdraw.com/.../result.png" }
]
}Latency & timeouts
Image gen typically takes 5–60 seconds depending on the model. In the default synchronous mode Kunavo holds the connection and polls the upstream for up to 540 seconds before returning a 504. Set your client-side timeout accordingly — or use the async API to submit in seconds and poll on your own schedule:
- Nano Banana / Nano Banana 2: usually under 10s.
- Nano Banana Pro / GPT-Image-2: 15–60s.
- Multi-reference + premium tier: up to ~2 minutes.
Async API (submit + poll / webhook)
Both image endpoints are synchronous by default — the call holds the connection until the upstream renders, then returns the standard OpenAI images response. For batch jobs, slow premium tiers, or serverless callers that can't hold a long request, add "async": true (or a webhook_url) to /v1/images/generations or /v1/images/edits.
The submit returns immediately with an img_* id; poll GET /v1/images/{id} until status is completed or failed — or register a webhook and skip polling entirely.
# Async: add "async": true (or a webhook_url) to either image endpoint.
# The call returns instantly with an img_ id instead of holding the connection.
import requests, time
API = "https://api.kunavo.com/v1"
H = {"Authorization": f"Bearer {API_KEY}"}
submit = requests.post(
f"{API}/images/generations",
headers=H,
json={"model": "gpt-image-2", "prompt": "a red panda", "async": True},
).json()
task_id = submit["id"] # "img_abc..."
status = submit["status"] # "queued"
# Poll GET /v1/images/{id} until terminal — or skip polling and use a webhook.
while status not in ("completed", "failed"):
time.sleep(3)
r = requests.get(f"{API}/images/{task_id}", headers=H).json()
status = r["status"]
if status == "completed":
print(r["output"]["url"])
else:
print("failed:", r["error"])Task object
| Field | Type | Notes |
|---|---|---|
id | string | img_* task id — pass to GET /v1/images/{id}. |
status | string | queued | in_progress | completed | failed. |
output | object|null | On completed: { url, urls, archived }. |
error | object|null | On failed: { code, message }. |
expires_at | int|null | Unix seconds; the row + media prune after ~30 days. |
GET /v1/images/{id} is owner-scoped: querying an id that isn't yours returns 404, indistinguishable from one that was never issued. Recommended polling cadence: 3 s with exponential backoff up to ~30 s.Webhooks
Pass webhook_url (https) on submit and Kunavo POSTs a signed image.completed / image.failed event when the task terminates — same envelope and signature as the video and music webhooks:
X-Kunavo-Webhook-Id— stable event id, identical across retries (use it to dedupe).X-Kunavo-Webhook-Timestamp— unix seconds the signature was computed at.X-Kunavo-Webhook-Signature—sha256=HMAC-SHA256 over{timestamp}.{rawBody}, keyed by your webhook secret.
Raw input escape hatch
For per-model knobs Kunavo doesn't map (seed, guidance scale, custom resolution enums), pass input verbatim and skip our adapter.
# For full control, pass kie-style 'input' verbatim. Skips our adapter.
resp = client.images.generate(
model="nano-banana-2",
prompt="", # ignored when input is set
extra_body={"input": {
"prompt": "a futuristic cyberpunk skyline at dusk",
"aspect_ratio": "16:9",
"resolution": "2K",
"output_format": "png",
}},
)input, you bypass Kunavo's validation. If the upstream rejects your fields you'll get a 422 from us — check error docs and the model's upstream documentation.