Lewati ke konten
kenari.

Audio, musik & video

kenari menyediakan endpoint audio, musik, dan video yang kompatibel dengan gaya OpenAI. Setiap endpoint hanya melayani model yang ditandai untuk endpoint itu di katalog. Memanggil endpoint audio dengan model teks akan mengembalikan status 400.

POST /v1/audio/speech

Kirim teks dan model, gateway mengembalikan audio mentah (bukan JSON). Biaya dihitung per 1.000 karakter input, lalu dipotong dari saldo Rupiah.

FieldTipeWajibKeterangan
modelstringyaId model TTS dari katalog.
inputstringyaTeks yang akan diucapkan. Ada batas panjangnya, lihat Batas panjang teks.
voicestringtidakSuara yang digunakan. Nama suara berbeda per model, jadi tidak ada daftar tunggal yang berlaku untuk semuanya. Lihat Memilih suara. Kosongkan untuk memakai suara default model.
response_formatstringtidakFormat audio: mp3, wav, pcm, opus, aac, atau flac. Tidak semua model bisa menghasilkan semuanya, jadi lihat Memilih format. Kosongkan untuk memakai format default model.
speednumbertidakKecepatan ucapan.
languagestringtidakKode bahasa, mis. id atau en. Default otomatis.

Setiap model TTS punya kosakata suaranya sendiri dan nama itu tidak saling menggantikan: suara yang sah di satu model akan ditolak di model lain. kenari meneruskan nilai voice apa adanya ke model.

Daftar suaranya ada di /v1/models, pada field voices milik model tersebut:

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

Elemen pertama pada voices adalah suara default, yang dipakai kalau voice dikosongkan. Nama dicocokkan tanpa memperhatikan huruf besar kecil.

Kalau sebuah model belum punya daftar suara tercatat, field voices tidak muncul. Untuk model seperti itu kenari tidak memeriksa nilai voice, jadi nilai apa pun yang diterima model akan diteruskan.

Mengirim suara di luar daftar yang diterbitkan akan dijawab 400 yang menyebut pilihan yang sah, jadi permintaannya tidak pernah sampai ke model dan tidak ada biaya yang terpotong.

Gaya bicara tidak punya field tersendiri. Tulis arahannya di depan teks, mis. Bacakan dengan gaya pembawa berita: ....

kenari bisa mengirim audio dalam mp3, wav, pcm, opus, aac, atau flac, tetapi satu model belum tentu bisa menghasilkan semuanya. Format yang benar-benar dilayani sebuah model ada di /v1/models, pada field formats:

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

Elemen pertama pada formats adalah format default, yang dipakai kalau response_format dikosongkan. Nama dicocokkan tanpa memperhatikan huruf besar kecil.

Kalau sebuah model belum punya daftar format tercatat, field formats tidak muncul. Untuk model seperti itu mp3, wav, dan pcm semuanya diterima, dan nilai lain diperlakukan sebagai mp3. Jadi opus, aac, dan flac hanya berlaku pada model yang menerbitkan formats. Kalau diminta pada model yang tidak menerbitkannya, hasilnya mp3 dengan header audio/mpeg yang sesuai.

Mengirim format di luar daftar yang diterbitkan akan dijawab 400 yang menyebut pilihan yang sah, dan tidak ada biaya yang terpotong. kenari tidak diam-diam menggantinya dengan format lain, karena Content-Type pada response mengikuti format yang diminta, jadi mengirim byte mp3 untuk permintaan wav akan memberi label yang salah pada file yang Anda terima.

Sintesis tidak dialirkan sepotong-sepotong, dan model punya batas waktu sendiri, jadi teks yang terlalu panjang tidak akan pernah selesai. Batas yang tercatat untuk sebuah model ada di /v1/models, pada field max_input_chars. Teks yang melewatinya dijawab 400 sebelum ada biaya yang terpotong.

Kalau field itu tidak muncul, kenari tidak membatasi panjangnya. Permintaan yang sangat panjang tetap bisa gagal karena batas waktu model, jadi pecah teks panjang menjadi beberapa permintaan.

Response berisi byte audio mentah dengan Content-Type sesuai format (audio/mpeg untuk mp3, audio/wav untuk wav, audio/pcm untuk pcm). Tidak ada JSON pembungkus.

Terminal window
curl https://kenari.id/v1/audio/speech \
-H "Authorization: Bearer kn-..." \
-H "Content-Type: application/json" \
-d '{"model":"gemini-3-1-flash-tts","input":"Halo, selamat datang di kenari."}' \
--output speech.mp3

POST /v1/audio/transcriptions

Kirim file audio dan model, gateway mengembalikan transkripsi teks. Biaya dihitung per detik durasi audio (dibulatkan ke atas), lalu dipotong dari saldo Rupiah. Durasi diperkirakan dari ukuran file saat request masuk dan diperbarui dengan durasi aktual yang dilaporkan provider.

Request menggunakan multipart/form-data.

FieldTipeWajibKeterangan
filefileyaFile audio yang akan ditranskripsi.
modelstringyaId model STT dari katalog.
languagestringtidakKode bahasa BCP-47, mis. id atau en. Deteksi otomatis jika tidak diisi.
promptstringtidakTeks petunjuk untuk memandu gaya transkripsi atau ejaan istilah tertentu.
temperaturenumbertidakSuhu sampling, 0..1.
response_formatstringtidakFormat respons: json (default), verbose_json, atau text.

Format json (default, atau jika response_format tidak diisi):

{ "text": "Halo, ini adalah transkripsi audio." }

Format verbose_json menambahkan task, language, duration, segments, dan words di samping text (bergantung model, field yang tidak dikirim model tidak muncul):

{
"text": "Halo, ini adalah transkripsi audio.",
"task": "transcribe",
"language": "id",
"duration": 8.5,
"segments": [
{
"id": 0, "seek": 0, "start": 0.0, "end": 3.2,
"text": "Halo, ini adalah transkripsi audio.",
"tokens": [50364, 1234],
"temperature": 0.0, "avg_logprob": -0.28,
"compression_ratio": 1.23, "no_speech_prob": 0.008
}
]
}

Response disusun ulang oleh kenari dari daftar field di atas, bukan diteruskan mentah dari provider, jadi field di luar daftar tersebut tidak ikut terkirim.

Format text mengembalikan string teks mentah tanpa JSON pembungkus.

Format srt dan vtt belum didukung di v1 dan akan memakai response json default.

Terminal window
curl https://kenari.id/v1/audio/transcriptions \
-H "Authorization: Bearer kn-..." \
-F file=@rekaman.mp3 \
-F model=whisper-1

POST /v1/music/generations

Kirim deskripsi atau lirik dan sebuah model musik, gateway mengembalikan satu lagu utuh dalam amplop JSON, dengan audio yang dikodekan sebagai base64. Biaya berupa tarif tetap per lagu, dipotong dari saldo Rupiah setelah lagu selesai dibuat.

Endpoint ini hanya melayani model yang ditandai sebagai model musik di katalog. Memanggilnya dengan model lain mengembalikan 400.

FieldTipeWajibKeterangan
modelstringyaId model musik dari katalog.
lyricsstringbersyaratLirik yang akan dinyanyikan. Wajib untuk lagu bernyanyi, yaitu saat instrumental bernilai false atau tidak diisi. Batas panjangnya per model, lihat di bawah.
promptstringbersyaratGaya dan suasana trek instrumental. Wajib saat instrumental bernilai true. Batas panjangnya per model, lihat di bawah.
instrumentalbooleantidakBuat trek tanpa vokal. Default false. Saat true, prompt wajib diisi dan lyrics diabaikan.
response_formatstringtidakFormat audio. Hanya mp3 yang tersedia untuk musik, dan itu sudah menjadi default. Mengirim wav atau pcm dijawab 400, bukan byte mp3 dengan Content-Type yang keliru.

Panjang lyrics dan prompt dihitung dalam karakter Unicode, bukan byte, jadi teks Indonesian beraksen tidak terpotong lebih cepat dari yang terlihat.

Batasnya berbeda per model, jadi bacalah dari /v1/models daripada menuliskannya tetap di kode Anda. Model musik yang batasnya sudah tercatat mengirim max_lyrics_chars dan max_prompt_chars pada entri modelnya. Permintaan yang melewatinya dijawab 400 sebelum ada biaya yang terpotong.

Kalau kedua field itu tidak muncul, kenari memakai batas bawaannya: 3.500 karakter untuk lyrics dan 2.000 karakter untuk prompt. Field yang tidak ada berarti belum ada angka yang tercatat untuk model itu, bukan berarti tanpa batas.

Panjang lagu ditentukan oleh modelnya dan tidak bisa diminta lewat API. Tidak ada parameter durasi pada request, dan tidak ada cara untuk meminta lagu yang lebih pendek atau lebih panjang.

Model yang panjangnya sudah tercatat mengirim max_duration_secs pada entri modelnya di /v1/models, dalam DETIK. Angka itu informasi saja: tidak ada permintaan yang ditolak karenanya. Panjang lagu yang sebenarnya biasanya lebih pendek dari angka itu.

lyrics adalah kata-kata yang dinyanyikan, bukan deskripsi tentang lagu. Menulis “lagu pop santai tentang Jakarta” akan membuat kalimat itu sendiri yang dinyanyikan. Untuk mendeskripsikan sebuah trek, pakai instrumental dengan prompt.

Penanda bagian membantu model menyusun lagu:

[Verse]
Lampu kota menyala satu per satu
Aku pulang membawa cerita baru
[Chorus]
Malam ini milik kita
Sampai pagi tiba

Sebagian besar lagu memerlukan dua sampai tiga menit, dan lagu yang lebih panjang memerlukan waktu lebih lama lagi. Gateway menahan koneksi tetap terbuka selama proses berjalan dan memberi satu pembuatan waktu hingga 15 menit sebelum menyerah, jadi tetapkan timeout klien Anda dengan longgar: lima menit adalah batas bawah yang masuk akal, dan klien yang berhenti di tiga menit sesekali akan memutus lagu yang sebentar lagi selesai. Baca seluruh body sebelum memprosesnya.

Response berupa JSON, dengan lagu ada di data[0].b64_json dan formatnya di data[0].format:

{
"data": [
{ "b64_json": "<base64 dari byte mp3>", "format": "mp3" }
]
}

format selalu mp3, dan array data selalu berisi tepat satu elemen. Audio tidak berupa tautan unduhan, decode base64-nya lalu tulis ke sebuah file.

Body mungkin datang diawali spasi ASCII. Gateway menulis satu spasi setiap 20 detik selama pembuatan lagu berjalan untuk menahan koneksi tetap terbuka, lalu mengirim bingkai JSON. Semua parser JSON melewati spasi di awal, jadi parse body sebagai JSON dan jangan pernah memperlakukannya sebagai blob mentah.

Kegagalan yang terjadi dalam masa tenggang tetap memakai status HTTP aslinya, 400, 402, 429, atau 503. Kegagalan yang terjadi setelahnya datang sebagai HTTP 200 dengan objek error OpenAI standar, misalnya {"error":{"code":"...","message":"...","param":null,"type":"..."}}. Selalu parse JSON-nya dan periksa ada tidaknya field error sebelum mendecode audio.

Satu akun boleh memiliki maksimal dua pembuatan lagu yang berjalan bersamaan. Permintaan ketiga dijawab 429 dengan header Retry-After, dan tidak ada biaya yang terpotong.

Terminal window
curl https://kenari.id/v1/music/generations \
-H "Authorization: Bearer kn-..." \
-H "Content-Type: application/json" \
-d '{
"model": "music-1.5",
"lyrics": "[Verse]\nLampu kota menyala satu per satu\n\n[Chorus]\nMalam ini milik kita"
}' \
--max-time 180 \
| jq -r '.data[0].b64_json' | base64 -d > lagu.mp3

Trek instrumental:

Terminal window
curl https://kenari.id/v1/music/generations \
-H "Authorization: Bearer kn-..." \
-H "Content-Type: application/json" \
-d '{"model":"music-1.5","instrumental":true,"prompt":"lo-fi santai, piano lembut, hujan malam"}' \
--max-time 180 \
| jq -r '.data[0].b64_json' | base64 -d > instrumen.mp3

Musik ditagih dengan tarif tetap per lagu, apa pun durasi hasilnya. Saldo ditahan saat permintaan diterima dan baru dipotong setelah audio berhasil dibuat. Kalau pembuatan gagal, tahanan itu dilepas dan saldo kembali utuh. Harga per lagu setiap model ada di katalog.


kenari menyediakan dua endpoint video. Keduanya asinkron: request diterima langsung, hasilnya diambil lewat polling.

POST /v1/videos/generations

FieldTipeWajibKeterangan
modelstringyaId model video dari katalog.
promptstringyaDeskripsi video yang diminta.
durationnumbertidakDurasi dalam detik. Default 6, rentang 1..15, kecuali model punya daftar durasi sendiri.
resolutionstringtidakTingkat resolusi, misalnya 720p atau 1080p. Default memakai tingkat pertama yang didaftarkan model.

Sebagian model video hanya menerima durasi tertentu, misalnya 4, 6, 8, dan 10 detik. Model seperti itu mengabaikan rentang 1..15 di atas dan memakai daftarnya sendiri. Nilai default adalah entri pertama pada daftar.

Resolusi bekerja dengan cara yang sama. Model yang menjual beberapa tingkat resolusi punya harga per detik yang berbeda untuk tiap tingkat, dan tingkat pertama adalah default. Model yang hanya punya satu harga menerima resolution apa pun dan mengabaikannya, karena tidak ada tingkat lain yang bisa dipilih.

Daftar durasi dan daftar resolusi untuk tiap model bisa dilihat di katalog. Nilai di luar daftar ditolak sebelum saldo dipotong, jadi request yang salah tidak pernah menghabiskan biaya.

POST /v1/videos/extensions

FieldTipeWajibKeterangan
modelstringyaId model video dari katalog.
video.urlstringyaURL klip sumber yang akan diperpanjang.
durationnumbertidakDurasi tambahan dalam detik. Default 6, rentang 1..15.

Kedua endpoint langsung mengembalikan objek pekerjaan:

{
"id": "vid_abc123",
"object": "video.job",
"status": "rendering",
"model": "kling-v2"
}

Klip belum tersedia. Gunakan id untuk polling.


GET /v1/videos/{id}

Polling sampai status bukan lagi rendering. Interval yang disarankan: setiap beberapa detik.

StatusArti
renderingPekerjaan masih berjalan, lanjutkan polling.
doneSelesai. Field url berisi tautan klip.
failedRender gagal. Biaya dikembalikan otomatis.
expiredPekerjaan kedaluwarsa sebelum diambil. Biaya dikembalikan otomatis.
{
"id": "vid_abc123",
"status": "done",
"url": "https://kenari.id/v1/videos/vid_abc123/content"
}

GET /v1/videos/{id}/content

Tautan pada field url dilayani oleh kenari, bukan oleh mesin render. Ambil dengan API key yang sama, dan hanya akun pemilik pekerjaan yang bisa membacanya.

Terminal window
curl -L https://kenari.id/v1/videos/vid_abc123/content \
-H "Authorization: Bearer kn-..." \
--output klip.mp4

Response berisi byte video mentah dengan Content-Type sesuai format klip (video/mp4, video/webm, atau video/quicktime). Selama pekerjaan belum selesai, endpoint ini menjawab 400.

Terminal window
# Buat video
JOB=$(curl -s https://kenari.id/v1/videos/generations \
-H "Authorization: Bearer kn-..." \
-H "Content-Type: application/json" \
-d '{"model":"kling-v2","prompt":"Seekor elang terbang di atas hutan hujan","duration":6}' \
| jq -r '.id')
# Poll sampai selesai
while true; do
RESULT=$(curl -s "https://kenari.id/v1/videos/$JOB" \
-H "Authorization: Bearer kn-...")
STATUS=$(echo "$RESULT" | jq -r '.status')
[ "$STATUS" != "rendering" ] && echo "$RESULT" && break
sleep 5
done

Video ditagih per detik durasi yang diminta, dipotong saat pekerjaan diterima. Jika render failed atau expired, biaya dikembalikan otomatis ke saldo. Lihat Penagihan untuk detail saldo dan pemotongan.

Kalau model menjual beberapa tingkat resolusi, tarif per detik mengikuti tingkat yang Anda minta, bukan tarif termurah. Biaya satu pekerjaan adalah durasi dikali tarif tingkat itu. Meminta tingkat yang tidak dijual model tersebut ditolak sebelum saldo dipotong, bukan diturunkan diam-diam ke tingkat yang lebih murah.