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
Pilih model
Section titled “Pilih model”Setiap model video menerima durasi dan resolusinya sendiri. Bacalah dari daftar model:
curl -s https://kenari.id/v1/models \ | jq '.data[] | select(.endpoints | index("videos")) | {id, video_durations, video_resolutions, pricing_lines}'Request
Section titled “Request”Generations
Section titled “Generations”POST /v1/videos/generations menerima body JSON dan memulai render.
| Field | Tipe | Wajib | Keterangan |
|---|---|---|---|
model | string | ya | Id model video, misalnya seedance-2.0-fast. |
prompt | string | ya | Deskripsi video. Beberapa model image-to-video juga menerima request dengan gambar tanpa prompt. |
duration | integer | tidak | Panjang dalam detik. Lihat Durasi dan resolusi. |
resolution | string | tidak | Tingkat resolusi, misalnya 720p. Default-nya tingkat pertama yang tercantum pada model. |
image_url | string | tidak | Gambar sumber untuk image-to-video. URL harus langsung mengembalikan gambarnya, karena redirect tidak diikuti. |
end_image_url | string | tidak | Frame terakhir, dipakai bersama image_url. |
input_images | array of strings | tidak | Satu atau lebih gambar sumber, sebagai alternatif image_url. |
video_url | string | tidak | Klip referensi untuk reference-to-video. Kalau request memuat klip dan gambar, request dirender sebagai reference render. |
aspect_ratio | string | tidak | Bentuk 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.
Extensions
Section titled “Extensions”POST /v1/videos/extensions melanjutkan klip yang sudah ada dari frame terakhirnya.
| Field | Tipe | Wajib | Keterangan |
|---|---|---|---|
model | string | ya | Id model video. |
video.url | string | ya | URL klip yang akan diperpanjang. |
prompt | string | tidak | Deskripsi perpanjangan. |
duration | integer | tidak | Tambahan panjang dalam detik. Aturannya sama dengan generations. |
resolution | string | tidak | Tingkat 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:
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.
Durasi dan resolusi
Section titled “Durasi dan resolusi”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.
Response
Section titled “Response”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"}Polling
Section titled “Polling”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.
| Status | Arti |
|---|---|
rendering | Pekerjaan masih berjalan. Lanjutkan polling. |
done | Klip siap. url berisi tautan unduhan. |
failed | Render gagal. Biaya dikembalikan. |
expired | Pekerjaan 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.
Unduh klip
Section titled “Unduh klip”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.
curl https://kenari.id/v1/videos/7b0e2c9a-4f1d-4c3e-9a55-2d8f6e1b0c47/content \ -H "Authorization: Bearer $KENARI_API_KEY" \ --output clip.mp4Contoh
Section titled “Contoh”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 1fi
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 5done
if [ "$STATUS" = "done" ]; then curl -s --fail "https://kenari.id/v1/videos/$JOB/content" \ -H "Authorization: Bearer $KENARI_API_KEY" \ --output clip.mp4else echo "$RESULT" | jq -r '.failure_reason'fiPython
Section titled “Python”import osimport 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"))JavaScript
Section titled “JavaScript”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);}Penagihan
Section titled “Penagihan”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.
| Status | Kode | Kapan |
|---|---|---|
| 400 | bad_request | Model 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. |
| 400 | bad_request | Pada endpoint unduhan: klip belum siap. |
| 400 | model_not_found | Id pekerjaan tidak ada atau milik akun lain. |
| 402 | insufficient_balance | Saldo tidak cukup untuk harga penuh pekerjaan. |
| 503 | upstream_error, all_providers_failed | Model 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.