Images
Generate an image from a text prompt, or edit existing images with a prompt. The endpoints are compatible with OpenAI Images, so the official OpenAI SDKs work with the base URL https://kenari.id/v1. Only image models are served here. Sending a text model returns 400.
POST /v1/images/generations
POST /v1/images/edits
Find the image models, with their prices, in the model list:
curl -s https://kenari.id/v1/models \ | jq '.data[] | select(.endpoints | index("images")) | {id, pricing_lines}'Request
Section titled “Request”Generations
Section titled “Generations”POST /v1/images/generations takes a JSON body.
| Field | Type | Required | Description |
|---|---|---|---|
model | string | yes | Image model id, for example gpt-image-2. |
prompt | string | yes | Description of the image. |
n | integer | no | Number of images, 1 to 10. Default 1. A value outside the range is moved to the nearest limit, and you are billed for that number. |
size | string | no | Image size, for example "1024x1024". A hint, not a guarantee. See Image size. |
background | string | no | "transparent", "opaque" or "auto". Any other value returns 400. Whether the image really has a transparent background depends on the model. |
response_format | string | no | "url" or "b64_json". Support varies by model, so leave it out to get the model’s default. See Response. |
POST /v1/images/edits changes existing images according to a prompt. The request is multipart/form-data because the images are sent as files. Repeat the image field (or image[]) once per source file.
| Field | Type | Required | Description |
|---|---|---|---|
model | string | yes | Image model id, for example gpt-image-2. |
image | file | yes | Source image. Send PNG, JPEG or WebP. Repeat the field for up to 16 files, each up to 25 MiB. |
prompt | string | yes | Description of the change. Up to 8,192 bytes. |
mask | file | no | A PNG mask, up to 25 MiB. As in OpenAI Images, transparent areas mark where the image is edited. Without a mask, the whole image can change. |
n | integer | no | Number of output images, 1 to 10. Default 1. It does not depend on how many source files you send. |
size | string | no | Image size, for example "1024x1024". A hint, not a guarantee. See Image size. |
background | string | no | "opaque" or "auto". "transparent" is not supported on edits and returns 400. Use generations for a transparent background. |
response_format | string | no | "url" or "b64_json". Same notes as generations. |
The whole request body can be up to 32 MiB. A 17th image, a file above its limit or a body above 32 MiB is rejected before anything is charged. Some models accept a single source image or no mask. Those models answer 400 when you send more.
Response
Section titled “Response”Both endpoints return the OpenAI Images shape: created and a data array with one item per image.
{ "created": 1718700000, "data": [ { "b64_json": "iVBORw0KGgoAAAANSUhEUg..." } ]}Each item carries b64_json, url, or both, depending on the model and on response_format. A url is a self-contained data: URI with the image inside it, not a link to a hosted file. Read whichever field is present and decode the base64 part. The response can also include usage, size, quality, output_format and a per-image revised_prompt when the model reports them.
Image size
Section titled “Image size”Models treat size in three ways. Some use it as given, some ignore it, and some understand only aspect ratios. For a ratio-based model, kenari maps the size you send to the nearest ratio the model supports, so "1792x1024" gives a wide 16:9 image at whatever pixel count that model produces, not exactly 1792x1024. The shape lands as close as the model allows, but the pixel count can differ. If exact pixel dimensions matter, check the dimensions of the returned image.
Long-running requests
Section titled “Long-running requests”An image can take several minutes. If the result is not ready within 45 seconds, kenari sends the 200 status and headers right away, then writes one space every 20 seconds to keep proxies from closing the connection, and finishes with the JSON body. Leading whitespace is valid JSON, so parse the complete body as usual. Set your client timeout to at least 5 minutes.
Examples
Section titled “Examples”curl https://kenari.id/v1/images/generations \ -H "Authorization: Bearer $KENARI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-image-2", "prompt": "A red panda writing code on a laptop", "n": 1, "size": "1024x1024" }' \ --max-time 300 \ --output response.json
if jq -e '.error' response.json > /dev/null; then jq -r '.error.message' response.json >&2else jq -r '.data[0] | .b64_json // (.url | split(",")[1])' response.json \ | base64 -d > image.pngfiPython
Section titled “Python”import base64import os
from openai import OpenAI
client = OpenAI( base_url="https://kenari.id/v1", api_key=os.environ["KENARI_API_KEY"],)
result = client.images.generate( model="gpt-image-2", prompt="A red panda writing code on a laptop", n=1, size="1024x1024",)
error = getattr(result, "error", None)if error or not result.data: raise RuntimeError(f"Image request failed: {error}")
item = result.data[0]b64 = item.b64_json or item.url.split(",", 1)[1]with open("image.png", "wb") as f: f.write(base64.b64decode(b64))JavaScript
Section titled “JavaScript”import fs from "node:fs";import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://kenari.id/v1", apiKey: process.env.KENARI_API_KEY,});
const result = await client.images.generate({ model: "gpt-image-2", prompt: "A red panda writing code on a laptop", n: 1, size: "1024x1024",});
if (result.error || !result.data?.length) { throw new Error(`Image request failed: ${JSON.stringify(result.error)}`);}
const item = result.data[0];const b64 = item.b64_json ?? item.url.split(",")[1];fs.writeFileSync("image.png", Buffer.from(b64, "base64"));Edit with curl
Section titled “Edit with curl”curl https://kenari.id/v1/images/edits \ -H "Authorization: Bearer $KENARI_API_KEY" \ -F "model=gpt-image-2" \ -F "image=@source.png" \ -F "image=@reference.png" \ -F "prompt=Change the sky to sunset, in the style of the second image" \ -F "n=1" \ --max-time 300 \ --output response.json
if jq -e '.error' response.json > /dev/null; then jq -r '.error.message' response.json >&2else jq -r '.data[0] | .b64_json // (.url | split(",")[1])' response.json \ | base64 -d > edited.pngfiEdit with Python
Section titled “Edit with Python”import base64import os
from openai import OpenAI
client = OpenAI( base_url="https://kenari.id/v1", api_key=os.environ["KENARI_API_KEY"],)
result = client.images.edit( model="gpt-image-2", image=[open("source.png", "rb"), open("reference.png", "rb")], prompt="Change the sky to sunset, in the style of the second image", n=1,)
error = getattr(result, "error", None)if error or not result.data: raise RuntimeError(f"Image request failed: {error}")
item = result.data[0]b64 = item.b64_json or item.url.split(",", 1)[1]with open("edited.png", "wb") as f: f.write(base64.b64decode(b64))Edit with JavaScript
Section titled “Edit with JavaScript”import fs from "node:fs";import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://kenari.id/v1", apiKey: process.env.KENARI_API_KEY,});
const result = await client.images.edit({ model: "gpt-image-2", image: [fs.createReadStream("source.png"), fs.createReadStream("reference.png")], prompt: "Change the sky to sunset, in the style of the second image", n: 1,});
if (result.error || !result.data?.length) { throw new Error(`Image request failed: ${JSON.stringify(result.error)}`);}
const item = result.data[0];const b64 = item.b64_json ?? item.url.split(",")[1];fs.writeFileSync("edited.png", Buffer.from(b64, "base64"));Billing
Section titled “Billing”Images are billed per image: n times the model’s price per image, taken from your balance. Four images cost four times one image, and the number of source files on an edit does not change the price. The amount is held when the request is accepted. You pay for the images actually delivered, so a provider that returns fewer than n images costs you less, and a response with no usable image is a failure that releases the whole hold. See How billing works for the rest.
Errors
Section titled “Errors”| Status | Code | When |
|---|---|---|
| 400 | bad_request | The model is not an image model, prompt, model or image is missing, background is invalid, or an edit has more than 16 images, a file above 25 MiB, or a prompt above 8,192 bytes. |
| 402 | insufficient_balance | The balance does not cover n images. |
| 413 | none | The request body is above 32 MiB. The response is plain text. |
| 503 | all_providers_failed, upstream_error | No provider could produce the image. Nothing is charged. Retry. |
See Errors for every other code.