Image Generations
curl --request POST \
--url https://kymaapi.com/v1/images/generations \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"model": "<string>",
"prompt": "<string>",
"n": 123,
"size": "<string>",
"quality": "<string>",
"image_url": "<string>",
"image_urls": [
"<string>"
],
"idempotency_key": "<string>",
"extra": {}
}
'import requests
url = "https://kymaapi.com/v1/images/generations"
payload = {
"model": "<string>",
"prompt": "<string>",
"n": 123,
"size": "<string>",
"quality": "<string>",
"image_url": "<string>",
"image_urls": ["<string>"],
"idempotency_key": "<string>",
"extra": {}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
model: '<string>',
prompt: '<string>',
n: 123,
size: '<string>',
quality: '<string>',
image_url: '<string>',
image_urls: ['<string>'],
idempotency_key: '<string>',
extra: {}
})
};
fetch('https://kymaapi.com/v1/images/generations', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://kymaapi.com/v1/images/generations",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'model' => '<string>',
'prompt' => '<string>',
'n' => 123,
'size' => '<string>',
'quality' => '<string>',
'image_url' => '<string>',
'image_urls' => [
'<string>'
],
'idempotency_key' => '<string>',
'extra' => [
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://kymaapi.com/v1/images/generations"
payload := strings.NewReader("{\n \"model\": \"<string>\",\n \"prompt\": \"<string>\",\n \"n\": 123,\n \"size\": \"<string>\",\n \"quality\": \"<string>\",\n \"image_url\": \"<string>\",\n \"image_urls\": [\n \"<string>\"\n ],\n \"idempotency_key\": \"<string>\",\n \"extra\": {}\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://kymaapi.com/v1/images/generations")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"model\": \"<string>\",\n \"prompt\": \"<string>\",\n \"n\": 123,\n \"size\": \"<string>\",\n \"quality\": \"<string>\",\n \"image_url\": \"<string>\",\n \"image_urls\": [\n \"<string>\"\n ],\n \"idempotency_key\": \"<string>\",\n \"extra\": {}\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://kymaapi.com/v1/images/generations")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"model\": \"<string>\",\n \"prompt\": \"<string>\",\n \"n\": 123,\n \"size\": \"<string>\",\n \"quality\": \"<string>\",\n \"image_url\": \"<string>\",\n \"image_urls\": [\n \"<string>\"\n ],\n \"idempotency_key\": \"<string>\",\n \"extra\": {}\n}"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"status": "<string>",
"estimated_cost": 123,
"charged_amount": {},
"refunded_amount": {}
}Endpoints
Image Generations
Generate or edit images. Asynchronous — returns 202 with a job id; poll GET /v1/jobs/ for the result.
POST
/
v1
/
images
/
generations
Image Generations
curl --request POST \
--url https://kymaapi.com/v1/images/generations \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"model": "<string>",
"prompt": "<string>",
"n": 123,
"size": "<string>",
"quality": "<string>",
"image_url": "<string>",
"image_urls": [
"<string>"
],
"idempotency_key": "<string>",
"extra": {}
}
'import requests
url = "https://kymaapi.com/v1/images/generations"
payload = {
"model": "<string>",
"prompt": "<string>",
"n": 123,
"size": "<string>",
"quality": "<string>",
"image_url": "<string>",
"image_urls": ["<string>"],
"idempotency_key": "<string>",
"extra": {}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
model: '<string>',
prompt: '<string>',
n: 123,
size: '<string>',
quality: '<string>',
image_url: '<string>',
image_urls: ['<string>'],
idempotency_key: '<string>',
extra: {}
})
};
fetch('https://kymaapi.com/v1/images/generations', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://kymaapi.com/v1/images/generations",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'model' => '<string>',
'prompt' => '<string>',
'n' => 123,
'size' => '<string>',
'quality' => '<string>',
'image_url' => '<string>',
'image_urls' => [
'<string>'
],
'idempotency_key' => '<string>',
'extra' => [
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://kymaapi.com/v1/images/generations"
payload := strings.NewReader("{\n \"model\": \"<string>\",\n \"prompt\": \"<string>\",\n \"n\": 123,\n \"size\": \"<string>\",\n \"quality\": \"<string>\",\n \"image_url\": \"<string>\",\n \"image_urls\": [\n \"<string>\"\n ],\n \"idempotency_key\": \"<string>\",\n \"extra\": {}\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://kymaapi.com/v1/images/generations")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"model\": \"<string>\",\n \"prompt\": \"<string>\",\n \"n\": 123,\n \"size\": \"<string>\",\n \"quality\": \"<string>\",\n \"image_url\": \"<string>\",\n \"image_urls\": [\n \"<string>\"\n ],\n \"idempotency_key\": \"<string>\",\n \"extra\": {}\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://kymaapi.com/v1/images/generations")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"model\": \"<string>\",\n \"prompt\": \"<string>\",\n \"n\": 123,\n \"size\": \"<string>\",\n \"quality\": \"<string>\",\n \"image_url\": \"<string>\",\n \"image_urls\": [\n \"<string>\"\n ],\n \"idempotency_key\": \"<string>\",\n \"extra\": {}\n}"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"status": "<string>",
"estimated_cost": 123,
"charged_amount": {},
"refunded_amount": {}
}Async endpoint. The request returns immediately with the job’s
Same response shape. Poll every 1–2 seconds until
The
Your jobs, newest first, in the same shape as above.
The
The examples below use models that need a first top-up.
The resulting
Replaying this exact request returns the same job — no new charge.
id; the actual generation runs in the background and is charged on success only.
minimax-image-01 is the only image model in the free tier, so it is the one an account that has never added credits can call. Every other image model needs a first top-up: until then it answers 402 with error.code: "model_requires_paid".Request body
string
required
One of:
minimax-image-01, gpt-image-2, gpt-image-2.5-flare, nano-banana-3-flash, recraft-v4, recraft-v4-pro, recraft-v4-vector, flux-2-pro, flux-kontext-pro, ideogram-v3. See Image Generation.string
required
Text prompt describing the desired image. Maximum 4000 characters.
number
default:"1"
Number of images to generate. Range 1–4; a value outside it is clamped, not refused. Each image is billed at the model’s per-image rate. Ignored by
flux-2-pro (per-megapixel billing — n=1 always).string
"WIDTHxHEIGHT" (e.g. "1024x1024") or "auto". Only the WIDTHxHEIGHT form is read, and it picks the nearest of the canonical aspect ratios 1:1, 16:9, 9:16, 4:3, 3:4, 21:9; the resolution is the model’s own for that aspect. An aspect string such as "16:9" is not parsed. "auto", an aspect string, or a ratio far from all six falls back to the model default, which is landscape 16:9 on recraft-v4, recraft-v4-pro, recraft-v4-vector and flux-2-pro (21:9 also becomes 16:9 there). On flux-2-pro the hold is sized from the WIDTHxHEIGHT you send, and the final charge from the megapixels of the image delivered, so the hold can be larger than the charge; the difference is refunded.string
low, medium or high on gpt-image-2 and gpt-image-2.5-flare, which price each image by quality. auto, or omitting it, is billed as medium. On gpt-image-2.5-flare each reference image (image_url / image_urls) adds $0.012 once per request, and sizes above 1536 px on a side return 400 invalid_parameter.string
HTTPS URL of a single source image. Required for
flux-kontext-pro (image-edit model). Optionally accepted by flux-2-pro for image-to-image generation. Ignored by other text-to-image models. A flux-kontext-pro request without image_url (or image_urls) returns 400 image_url_required.string[]
Array of HTTPS source image URLs for multi-reference blending. Up to 16 on
gpt-image-2 and gpt-image-2.5-flare and up to 10 on every other model. On flux-2-pro the first 6 entries are processed at high fidelity, entries 7–10 act as supplementary composition guides. Models that take image input: flux-2-pro, flux-kontext-pro, gpt-image-2, gpt-image-2.5-flare, nano-banana-3-flash. When both image_url and image_urls are present, image_urls takes precedence. A longer array returns 400 too_many_references.string
Client-supplied key to make the request safe to retry. Also accepted as an
Idempotency-Key header. Keys longer than 128 characters are cut to 128. The key is scoped to your account, not to one API key: repeated POSTs from the same account with the same key return the same job — no duplicate charge, no duplicate generation.object
Pass-through model-specific parameters (e.g.
seed, style). Forwarded to the underlying provider, except the fields your price is based on or that are moderated (prompt, image count, size, duration, reference images): those always come from the top-level parameters, and provider quality tiers that are not priced separately run at the priced tier.Response
202 Accepted for new jobs, 200 OK if the same idempotency_key is replayed.
When all of your concurrent image slots are busy, the job waits in a queue instead, and the response is 200 OK with a short body. Poll it like any other job; the hold is placed when it leaves the queue.
{ "id": "f3a9c8b2e1d0a4b6c7d8e9f0", "status": "queued", "queue_position": 1, "model": "minimax-image-01", "created": 1777111200 }
{
"id": "f3a9c8b2e1d0a4b6c7d8e9f0",
"object": "image.job",
"status": "pending",
"model": "recraft-v4",
"created_at": "2026-04-25T10:00:00.000Z",
"completed_at": null,
"output": null,
"error": null,
"estimated_cost": 0.054,
"charged_amount": null
}
string
Job ID. Use it to poll
GET /v1/jobs/{id}.string
One of:
queued, pending, processing, succeeded, failed, refunded, expired.number
Hold amount placed at submit time (USD). Refunded if the job fails.
number | null
Final billed amount once the job succeeds.
null until then.number | null
The part of the hold returned to your balance once it is released (USD).
null until then.Poll for the result
GET /v1/jobs/{id}
status is succeeded, failed, refunded, or expired. Most images complete within 30 seconds; the runner times out at 5 minutes.
{
"id": "f3a9c8b2e1d0a4b6c7d8e9f0",
"object": "image.job",
"status": "succeeded",
"model": "recraft-v4",
"created_at": "2026-04-25T10:00:00.000Z",
"completed_at": "2026-04-25T10:00:14.220Z",
"output": {
"url": "https://…/image.jpg",
"metadata": { "images": [{ "width": 1920, "height": 1080, "content_type": "image/jpeg" }] }
},
"error": null,
"estimated_cost": 0.054,
"charged_amount": 0.054
}
output.url is a Kyma-hosted URL. The image is copied there so the link keeps working after the model’s own temporary link expires. Job responses also carry kind, capability, prompt, aspect_ratio, duration_s and is_shared.
List your jobs
GET /v1/jobs?kind=image&limit=20
kind is image, video or audio (optional); limit is 1–100 (default 20). The response is {"object": "list", "data": [...], "next_cursor", "has_more"}: pass next_cursor back as cursor for the next page.
Errors
Most errors have the shape{"error": {"message", "code"}}. Some also carry type.
| Status | error.type | error.code | When |
|---|---|---|---|
400 | invalid_request | — | Invalid JSON body |
400 | — | invalid_model | The model is served, but not by the image pipeline |
400 | — | invalid_prompt | Missing prompt or > 4000 chars |
400 | — | image_url_required | flux-kontext-pro called without image_url or image_urls |
400 | — | too_many_references | image_urls has more entries than the model accepts |
401 | auth_error | — | Missing or invalid API key |
402 | billing_error | model_requires_paid | Your account has never added credits and the model is not in the free tier. The message lists the free-tier media models |
402 | billing_error | insufficient_credits | Your balance does not cover the job |
402 | billing_error | insufficient_credits | The hold could not be placed after the job was created |
404 | not_found_error | model_not_found | Kyma does not serve that model. A retired model’s message names its replacement (Model imagen-4 was retired. Use nano-banana-3-flash.) and error.did_you_mean lists it (error.didYouMean is kept for older clients) |
413 | invalid_request | — | Request body > 25 MB |
429 | — | queue_full | Every concurrent slot for your tier is busy and your queue is full. Retry-After: 30. Tier caps are on GET /v1/limits/tiers |
500 | — | db_error / billing_error | Internal failure — nothing is charged |
402 bodies also carry balance, required, topup_url and a details object at the top level, plus topup on model_requires_paid and insufficient_credits.
When status becomes failed or refunded, no charge is made and the held funds are returned to your balance via a hold_release ledger entry.
Examples
Free-tier image
Works on the signup credit.curl -X POST https://kymaapi.com/v1/images/generations \
-H "Authorization: Bearer $KYMA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "minimax-image-01",
"prompt": "a lighthouse on a rocky coast at dawn, soft mist",
"size": "1024x1024"
}'
Cinematic hero shot
curl -X POST https://kymaapi.com/v1/images/generations \
-H "Authorization: Bearer $KYMA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "flux-2-pro",
"prompt": "an architectural photo of a modernist house at dusk, long exposure",
"size": "1920x1080"
}'
Image edit with kontext
curl -X POST https://kymaapi.com/v1/images/generations \
-H "Authorization: Bearer $KYMA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "flux-kontext-pro",
"prompt": "replace the sky with a stormy sunset",
"image_url": "https://example.com/source.jpg"
}'
Multi-reference blend with FLUX.2 Pro
curl -X POST https://kymaapi.com/v1/images/generations \
-H "Authorization: Bearer $KYMA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "flux-2-pro",
"prompt": "blend these references into one cohesive editorial scene",
"image_urls": [
"https://example.com/style-ref-a.jpg",
"https://example.com/style-ref-b.jpg",
"https://example.com/composition-c.jpg"
],
"size": "1920x1080"
}'
Native vector (SVG) output
curl -X POST https://kymaapi.com/v1/images/generations \
-H "Authorization: Bearer $KYMA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "recraft-v4-vector",
"prompt": "minimalist Greek psi symbol shaped like a wave, gold on navy"
}'
output.url returns an SVG file (Content-Type: image/svg+xml) with editable paths and layers — open directly in Figma or Illustrator.
Idempotent retry
curl -X POST https://kymaapi.com/v1/images/generations \
-H "Authorization: Bearer $KYMA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "ideogram-v3",
"prompt": "minimalist coffee bag label that reads KYMA ROAST",
"idempotency_key": "order-78421-asset-1"
}'