Docs

Docs

Image generation

Two endpoints: text-to-image and image edit / image-to-image. Both return permanent result URLs: Kunavo archives every image to its own CDN.

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 permanent files.kunavo.com links: Kunavo archives every image to its own CDN, so unlike raw upstream URLs they never expire. If an image cannot be archived, its URL is the upstream's temporary one, on another host: download that image and re-host it.

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.067 · 4K $0.122K
gpt-image-21K / 2K / 4K — $0.0633 (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.

Response shape

Both endpoints return OpenAI's standard images response:

{
  "created": 1779360636,
  "data": [
    { "url": "https://files.kunavo.com/image/.../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 }. archived is false when an image could not be archived and its URL is the upstream's temporary one.
errorobject|nullOn failed: { code, message }.
expires_atint|nullUnix seconds, 30 days after submission: the task can be polled at least until then. Its result URLs do not expire.
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.
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, negative prompt, output format), pass input. It is sent upstream as written, in place of the input our adapter would build.

The field that selects a model's price tier is the exception: resolution, or rendering_speed on Ideogram. On a model with tiers it is always sent as one of the model's tiers, and you are billed for that tier. When input leaves it out, or gives a value that is not one of the model's tiers, the default tier is sent and billed. The same field at the top level of the request is not read when input is set.

# Raw upstream 'input': sent as written, in place of what our adapter builds.
# "resolution" sets the billed tier: 2K here, the default tier when left out.
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