Video Generations
curl --request POST \
--url https://kymaapi.com/v1/videos/generations \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"model": "<string>",
"prompt": "<string>",
"duration_s": 123,
"aspect_ratio": "<string>",
"image_url": "<string>",
"audio_url": "<string>",
"idempotency_key": "<string>",
"extra": {}
}
'import requests
url = "https://kymaapi.com/v1/videos/generations"
payload = {
"model": "<string>",
"prompt": "<string>",
"duration_s": 123,
"aspect_ratio": "<string>",
"image_url": "<string>",
"audio_url": "<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>',
duration_s: 123,
aspect_ratio: '<string>',
image_url: '<string>',
audio_url: '<string>',
idempotency_key: '<string>',
extra: {}
})
};
fetch('https://kymaapi.com/v1/videos/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/videos/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>',
'duration_s' => 123,
'aspect_ratio' => '<string>',
'image_url' => '<string>',
'audio_url' => '<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/videos/generations"
payload := strings.NewReader("{\n \"model\": \"<string>\",\n \"prompt\": \"<string>\",\n \"duration_s\": 123,\n \"aspect_ratio\": \"<string>\",\n \"image_url\": \"<string>\",\n \"audio_url\": \"<string>\",\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/videos/generations")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"model\": \"<string>\",\n \"prompt\": \"<string>\",\n \"duration_s\": 123,\n \"aspect_ratio\": \"<string>\",\n \"image_url\": \"<string>\",\n \"audio_url\": \"<string>\",\n \"idempotency_key\": \"<string>\",\n \"extra\": {}\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://kymaapi.com/v1/videos/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 \"duration_s\": 123,\n \"aspect_ratio\": \"<string>\",\n \"image_url\": \"<string>\",\n \"audio_url\": \"<string>\",\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
Video Generations
Generate video clips with veo-3.1-fast, kling-3-pro or kling-avatar-v2-pro. Async, billed per second of footage.
POST
/
v1
/
videos
/
generations
Video Generations
curl --request POST \
--url https://kymaapi.com/v1/videos/generations \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"model": "<string>",
"prompt": "<string>",
"duration_s": 123,
"aspect_ratio": "<string>",
"image_url": "<string>",
"audio_url": "<string>",
"idempotency_key": "<string>",
"extra": {}
}
'import requests
url = "https://kymaapi.com/v1/videos/generations"
payload = {
"model": "<string>",
"prompt": "<string>",
"duration_s": 123,
"aspect_ratio": "<string>",
"image_url": "<string>",
"audio_url": "<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>',
duration_s: 123,
aspect_ratio: '<string>',
image_url: '<string>',
audio_url: '<string>',
idempotency_key: '<string>',
extra: {}
})
};
fetch('https://kymaapi.com/v1/videos/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/videos/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>',
'duration_s' => 123,
'aspect_ratio' => '<string>',
'image_url' => '<string>',
'audio_url' => '<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/videos/generations"
payload := strings.NewReader("{\n \"model\": \"<string>\",\n \"prompt\": \"<string>\",\n \"duration_s\": 123,\n \"aspect_ratio\": \"<string>\",\n \"image_url\": \"<string>\",\n \"audio_url\": \"<string>\",\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/videos/generations")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"model\": \"<string>\",\n \"prompt\": \"<string>\",\n \"duration_s\": 123,\n \"aspect_ratio\": \"<string>\",\n \"image_url\": \"<string>\",\n \"audio_url\": \"<string>\",\n \"idempotency_key\": \"<string>\",\n \"extra\": {}\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://kymaapi.com/v1/videos/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 \"duration_s\": 123,\n \"aspect_ratio\": \"<string>\",\n \"image_url\": \"<string>\",\n \"audio_url\": \"<string>\",\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 2–5 seconds until
The
When
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. Billed per second of generated footage.
Request body
string
required
veo-3.1-fast (Google, 720p with native audio, 4/6/8s), kling-3-pro (Kuaishou Kling 3 Pro, silent, 3–15s) or kling-avatar-v2-pro (Kuaishou Kling AI Avatar v2 Pro: one portrait + one PCM WAV voice or song track → a lip-synced clip as long as the audio). Other video ids are paused and return 404 model_not_found. See Video Generation.string
required
Text prompt describing the desired video. Maximum 4000 characters.
number
Clip length in seconds. Each model accepts its own set of lengths; a value it does not accept is snapped to one it does, not refused. When omitted, the model’s shortest length is used. Each second is billed at the model’s per-second rate.For
kling-avatar-v2-pro it is required and means something else: the length of your audio in seconds, rounded up (5 to 60). The clip runs as long as the audio and you are charged for the clip’s real length; duration_s caps that charge. Audio longer than duration_s is cut at duration_s before generation.string
Aspect ratio of the clip, for example
"16:9" or "9:16".string
HTTPS URL of a starting frame. When present, the image becomes the first frame and the prompt drives the motion. Optional, except on
kling-avatar-v2-pro, where it is required and is the face to animate.string
kling-avatar-v2-pro only, and required there: HTTPS URL of the speech or song to lip-sync, as an uncompressed PCM WAV file (WAV PCM only for now: MP3, M4A, AAC and OGG are refused), at least 5 seconds. The clip plays this audio unchanged. Any other model refuses it with 400 invalid_parameter.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: repeated POSTs with the same key return the same job — no duplicate charge, no duplicate generation.object
Model-specific parameters. On
kling-3-pro only cfg_scale (a number from 0 to 1) is forwarded and every other field is ignored, so multi-shot prompts, reference elements, end frames and voices cannot change what a job costs or bypass prompt moderation. On other models it is a pass-through (e.g. seed), 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.
{
"id": "v3a9c8b2e1d0a4b6c7d8e9f0",
"object": "video.job",
"status": "pending",
"model": "veo-3.1-fast",
"created_at": "2026-04-26T10:00:00.000Z",
"completed_at": null,
"output": null,
"error": null,
"estimated_cost": 0.54,
"charged_amount": null
}
string
Job ID. Use it to poll
GET /v1/jobs/{id}.string
One of:
pending, processing, succeeded, failed, refunded, expired.number
Hold amount placed at submit time (USD) —
per_second_usd × duration_s. 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 clips complete within 1–3 minutes; the runner times out at 10 minutes.
{
"id": "v3a9c8b2e1d0a4b6c7d8e9f0",
"object": "video.job",
"status": "succeeded",
"model": "veo-3.1-fast",
"created_at": "2026-04-26T10:00:00.000Z",
"completed_at": "2026-04-26T10:02:18.840Z",
"output": {
"url": "https://…/clip.mp4",
"metadata": {
"videos": [{
"width": 1280,
"height": 720,
"duration": 4,
"content_type": "video/mp4"
}]
}
},
"error": null,
"estimated_cost": 0.54,
"charged_amount": 0.54
}
output.url is a Kyma-hosted URL. The clip is copied there so the link keeps working after the model’s own temporary link expires.
Errors
| Status | error.type | error.code | When |
|---|---|---|---|
404 | not_found_error | model_not_found | Kyma does not serve that model (any video id other than veo-3.1-fast and kling-3-pro today). A retired model’s message names its replacement, and error.did_you_mean lists it |
400 | invalid_request | not_a_video_model | The model is served, but it does not make video |
400 | — | invalid_prompt | Missing prompt or > 4000 chars |
401 | auth_error | — | Missing or invalid API key |
402 | billing_error | model_requires_paid / insufficient_credits | As on image generation |
413 | invalid_request | — | Request body > 25 MB |
429 | — | video_rate_exceeded | Tier 0: video needs a paid tier |
429 | — | queue_full | Every slot is busy and the queue is full. Retry-After: 30 |
500 | — | db_error / billing_error | Internal failure — nothing is charged |
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
Talking-head with native audio
curl -X POST https://kymaapi.com/v1/videos/generations \
-H "Authorization: Bearer $KYMA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "veo-3.1-fast",
"prompt": "a barista hands over a cup and says \"your flat white\", the machine hisses, ambient cafe murmur",
"duration_s": 6
}'
Cinematic hero clip
curl -X POST https://kymaapi.com/v1/videos/generations \
-H "Authorization: Bearer $KYMA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "kling-3-pro",
"prompt": "an architectural fly-through of a glass-and-steel tower at sunset, cinematic anamorphic look",
"duration_s": 5
}'
Image-to-video
curl -X POST https://kymaapi.com/v1/videos/generations \
-H "Authorization: Bearer $KYMA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "veo-3.1-fast",
"prompt": "the camera pulls back as the subject begins to walk forward",
"image_url": "https://example.com/frame-1.jpg",
"duration_s": 4
}'
Idempotent retry
curl -X POST https://kymaapi.com/v1/videos/generations \
-H "Authorization: Bearer $KYMA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "veo-3.1-fast",
"prompt": "a phone notification slides in from the right, soft chime, clean UI background",
"duration_s": 4,
"idempotency_key": "campaign-spring-asset-7"
}'