Lewati ke konten
kenari.

Video

Buat video dari prompt, atau perpanjang klip yang sudah ada. Endpoint video bersifat asinkron. Request langsung mengembalikan id pekerjaan, kamu melakukan polling sampai pekerjaan selesai, lalu mengunduh klipnya. Hanya model video yang dilayani di sini. Mengirim model lain mengembalikan 400.

POST /v1/videos/generations

POST /v1/videos/extensions

GET /v1/videos/{id}

GET /v1/videos/{id}/content

Setiap model video menerima durasi dan resolusinya sendiri. Bacalah dari daftar model:

Terminal window
curl -s https://kenari.id/v1/models \
| jq '.data[] | select(.endpoints | index("videos")) | {id, video_durations, video_resolutions, pricing_lines}'

POST /v1/videos/generations menerima body JSON dan memulai render.

FieldTipeWajibKeterangan
modelstringyaId model video, misalnya seedance-2.0-fast.
promptstringyaDeskripsi video. Beberapa model image-to-video juga menerima request dengan gambar tanpa prompt.
durationintegertidakPanjang dalam detik. Lihat Durasi dan resolusi.
resolutionstringtidakTingkat resolusi, misalnya 720p. Default-nya tingkat pertama yang tercantum pada model.
image_urlstringtidakGambar sumber untuk image-to-video. URL harus langsung mengembalikan gambarnya, karena redirect tidak diikuti.
end_image_urlstringtidakFrame terakhir, dipakai bersama image_url.
input_imagesarray of stringstidakSatu atau lebih gambar sumber, sebagai alternatif image_url.
video_urlstringtidakKlip referensi untuk reference-to-video. Kalau request memuat klip dan gambar, request dirender sebagai reference render.
aspect_ratiostringtidakBentuk output, misalnya 16:9 atau 9:16. kenari tidak memvalidasinya. Nilainya diteruskan ke model apa adanya, dan efeknya bergantung pada model.

Mengirim gambar memilih varian image-to-video milik model, dan mengirim klip memilih reference-to-video. Untuk pemilihan ini, last_image_url dihitung seperti end_image_url, dan reference_video_url dihitung seperti video_url. Model yang tidak bisa merender dari input yang kamu kirim menjawab 400 dan menyebut apa yang diterimanya.

POST /v1/videos/extensions melanjutkan klip yang sudah ada dari frame terakhirnya.

FieldTipeWajibKeterangan
modelstringyaId model video.
video.urlstringyaURL klip yang akan diperpanjang.
promptstringtidakDeskripsi perpanjangan.
durationintegertidakTambahan panjang dalam detik. Aturannya sama dengan generations.
resolutionstringtidakTingkat resolusi, misalnya 720p. Aturannya sama dengan generations: default-nya tingkat pertama yang tercantum pada model, dan tingkat di luar daftar dijawab 400. Harga bergantung padanya.

Body-nya JSON. Tidak semua model video bisa memperpanjang klip. Cari yang bisa:

Terminal window
curl -s https://kenari.id/v1/models | jq -r '.data[] | select(.video_extensions) | .id'

Kalau perintah itu tidak mencetak apa pun, saat ini tidak ada model yang bisa memperpanjang dan endpoint extensions menjawab 503. Kalau ada hasilnya, isi model dengan salah satu id yang tercetak. Request ini memperpanjang sebuah klip:

{
"model": "<model-id>",
"video": { "url": "https://example.com/clip.mp4" },
"prompt": "Elang itu hinggap di dahan",
"duration": 5,
"resolution": "720p"
}

Ganti <model-id> dengan id dari perintah di atas. Model yang entri-nya tidak memiliki video_extensions menjawab 503, dan tidak ada biaya yang terpotong.

Model yang mencantumkan video_durations hanya menerima nilai itu. Entri pertama adalah default, dan nilai lain dijawab 400 beserta daftar yang diizinkan. Model tanpa daftar itu menerima duration dari 1 sampai 15 dengan default 6, dan memindahkan nilai di luar rentang ke batas terdekat. duration harus berupa bilangan bulat dalam detik.

Model yang mencantumkan video_resolutions hanya menerima tingkat itu, dan yang pertama adalah default. Nama tingkat dicocokkan tanpa memperhatikan huruf besar atau kecil. Tingkat di luar daftar dijawab 400 beserta tingkat yang tersedia. kenari tidak pernah memindahkanmu ke tingkat yang lebih murah. Model yang tidak mencantumkan tingkat hanya punya satu harga dan mengabaikan resolution.

Durasi atau resolusi yang tidak valid ditolak sebelum ada biaya yang terpotong.

Kedua endpoint pembuatan langsung mengembalikan pekerjaannya. Klipnya belum ada.

{
"id": "7b0e2c9a-4f1d-4c3e-9a55-2d8f6e1b0c47",
"object": "video.job",
"status": "rendering",
"model": "seedance-2.0-fast"
}

GET /v1/videos/{id} mengembalikan keadaan terbaru sebuah pekerjaan. Lakukan polling setiap beberapa detik sampai status bukan lagi rendering. Hanya akun yang membuat pekerjaan yang bisa membacanya.

StatusArti
renderingPekerjaan masih berjalan. Lanjutkan polling.
doneKlip siap. url berisi tautan unduhan.
failedRender gagal. Biaya dikembalikan.
expiredPekerjaan tidak selesai tepat waktu. Biaya dikembalikan.

Poll yang mengembalikan 429 atau 503 adalah kegagalan HTTP sementara, bukan pekerjaan yang gagal. Tunggu lalu lakukan polling lagi. Status error lain menghentikan loop. Selain done, hanya failed dan expired yang merupakan status akhir pekerjaan.

Pekerjaan yang selesai:

{
"id": "7b0e2c9a-4f1d-4c3e-9a55-2d8f6e1b0c47",
"status": "done",
"url": "https://kenari.id/v1/videos/7b0e2c9a-4f1d-4c3e-9a55-2d8f6e1b0c47/content"
}

Pekerjaan failed atau expired tidak punya url. Sebagai gantinya ada failure_reason, pesan singkat berbahasa Indonesia yang menjelaskan penyebabnya, misalnya kapasitas sedang penuh dan kamu bisa mencoba lagi, atau prompt atau gambar ditolak oleh kebijakan konten.

Pekerjaan yang belum selesai setelah satu jam ditutup sebagai expired dan biayanya dikembalikan.

GET /v1/videos/{id}/content mengalirkan klip yang sudah selesai. Tautan di url dilayani oleh kenari dan membutuhkan API key valid milik akun yang membuat pekerjaan. Akun lain tidak bisa mengunduhnya. Content-Type-nya video/mp4, video/webm atau video/quicktime. Selama pekerjaan masih berjalan, endpoint ini menjawab 400.

Terminal window
curl https://kenari.id/v1/videos/7b0e2c9a-4f1d-4c3e-9a55-2d8f6e1b0c47/content \
-H "Authorization: Bearer $KENARI_API_KEY" \
--output clip.mp4
Terminal window
JOB=$(curl -s https://kenari.id/v1/videos/generations \
-H "Authorization: Bearer $KENARI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "seedance-2.0-fast",
"prompt": "Seekor elang terbang di atas hutan hujan saat matahari terbit",
"duration": 5,
"resolution": "720p"
}' | jq -r '.id // empty')
if [ -z "$JOB" ]; then
echo "The job was not created" >&2
exit 1
fi
while true; do
RESULT=$(curl -s "https://kenari.id/v1/videos/$JOB" \
-H "Authorization: Bearer $KENARI_API_KEY")
STATUS=$(echo "$RESULT" | jq -r '.status // empty')
if [ -z "$STATUS" ]; then
echo "$RESULT" >&2
exit 1
fi
echo "status: $STATUS"
[ "$STATUS" != "rendering" ] && break
sleep 5
done
if [ "$STATUS" = "done" ]; then
curl -s --fail "https://kenari.id/v1/videos/$JOB/content" \
-H "Authorization: Bearer $KENARI_API_KEY" \
--output clip.mp4
else
echo "$RESULT" | jq -r '.failure_reason'
fi
import os
import time
import requests
BASE = "https://kenari.id/v1"
headers = {"Authorization": f"Bearer {os.environ['KENARI_API_KEY']}"}
job = requests.post(
f"{BASE}/videos/generations",
headers=headers,
json={
"model": "seedance-2.0-fast",
"prompt": "Seekor elang terbang di atas hutan hujan saat matahari terbit",
"duration": 5,
"resolution": "720p",
},
timeout=60,
)
job.raise_for_status()
job_id = job.json()["id"]
while True:
poll = requests.get(f"{BASE}/videos/{job_id}", headers=headers, timeout=30)
if poll.status_code in (429, 503):
time.sleep(5)
continue
poll.raise_for_status()
state = poll.json()
print("status:", state["status"])
if state["status"] != "rendering":
break
time.sleep(5)
if state["status"] == "done":
clip = requests.get(f"{BASE}/videos/{job_id}/content", headers=headers, timeout=300)
clip.raise_for_status()
with open("clip.mp4", "wb") as f:
f.write(clip.content)
else:
print(state.get("failure_reason"))
import fs from "node:fs";
const BASE = "https://kenari.id/v1";
const headers = {
Authorization: `Bearer ${process.env.KENARI_API_KEY}`,
"Content-Type": "application/json",
};
const created = await fetch(`${BASE}/videos/generations`, {
method: "POST",
headers,
body: JSON.stringify({
model: "seedance-2.0-fast",
prompt: "Seekor elang terbang di atas hutan hujan saat matahari terbit",
duration: 5,
resolution: "720p",
}),
});
if (!created.ok) throw new Error(await created.text());
const { id } = await created.json();
let state;
do {
await new Promise((resolve) => setTimeout(resolve, 5000));
const poll = await fetch(`${BASE}/videos/${id}`, { headers });
if (poll.status === 429 || poll.status === 503) {
state = { status: "rendering" };
continue;
}
if (!poll.ok) throw new Error(await poll.text());
state = await poll.json();
console.log("status:", state.status);
} while (state.status === "rendering");
if (state.status === "done") {
const clip = await fetch(`${BASE}/videos/${id}/content`, { headers });
if (!clip.ok) throw new Error(await clip.text());
fs.writeFileSync("clip.mp4", Buffer.from(await clip.arrayBuffer()));
} else {
console.log(state.failure_reason);
}

Video ditagih per detik durasi yang diminta, dengan harga tingkat resolusi yang kamu pilih. Render 5 detik pada 720p berharga 5 kali harga per detik 720p milik model. Setiap tingkat punya harganya sendiri di pricing_lines pada GET /v1/models. Seluruh jumlahnya diambil dari saldo saat pekerjaan diterima. Kalau pekerjaan berakhir failed atau expired, biayanya dikembalikan otomatis, baik kamu terus melakukan polling maupun tidak. Selengkapnya ada di Cara penagihan.

StatusKodeKapan
400bad_requestModel bukan model video, prompt atau video.url tidak ada, durasi atau resolusi tidak tersedia untuk model, atau model tidak bisa merender dari input yang kamu kirim.
400bad_requestPada endpoint unduhan: klip belum siap.
400model_not_foundId pekerjaan tidak ada atau milik akun lain.
402insufficient_balanceSaldo tidak cukup untuk harga penuh pekerjaan.
503upstream_error, all_providers_failedModel sedang tidak tersedia, atau pada /v1/videos/extensions entri model tidak memiliki video_extensions. Tidak ada biaya yang terpotong. Coba lagi sekali atau dua kali, dan kalau terus gagal pada extension, pilih model yang mencantumkan video_extensions.

Kode lainnya ada di Error.