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.

Tài liệu này bằng tiếng Anh. Để xem hướng dẫn bắt đầu nhanh bằng tiếng Việt, hãy xem:Hướng dẫn tiếng Việt — cách lấy Gemini API key

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

ParamTypeNotes
modelstringAny image model slug from /v1/models.
promptstringText prompt. Always required.
sizestringOpenAI-style "1024x1024". Mapped to upstream aspect-ratio.
qualitystandard|hdWhere supported (gpt-image).
resolution1K|2K|4KResolution tier for tiered models — see Resolution tiers below. Billed by tier.
nintNumber of images. Most models return 1 regardless.
inputobjectEscape hatch — see Raw input below.
Result URLs are temporary (typically 24h TTL). For production, persist the bytes to your own object storage immediately on success.

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 eachDefault
nano-banana-21K $0.0469 · 2K $0.0707 · 4K $0.10571K
nano-banana-pro1K/2K $0.0938 · 4K $0.1682K
gpt-image-21K / 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.

GPT-Image-2 cannot render 4K at a 1:1 (square) aspect ratio. If you request 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 edit
  • gpt-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 (or image_url for 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}",
    },
)
The same applies to 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

FieldTypeNotes
idstringimg_* task id — pass to GET /v1/images/{id}.
statusstringqueued | in_progress | completed | failed.
outputobject|nullOn completed: { url, urls, archived }.
errorobject|nullOn failed: { code, message }.
expires_atint|nullUnix 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-Signaturesha256= HMAC-SHA256 over {timestamp}.{rawBody}, keyed by your webhook secret.
Delivery retries with backoff over ~9 h. The async row stays pollable the whole time, so a missed webhook is always recoverable via GET.

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",
    }},
)
When you pass 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.

Where to go next