Referensi API
Buat video AI secara programatis. Kirim prompt, poll hingga selesai, lalu dapatkan URL video langsung — semuanya lewat REST.
URL Dasar: https://yoh.appContoh kode akan menggunakan placeholder sk_live_YOUR_API_KEY sampai Anda masuk.
Mulai Cepat
Buat video pertama Anda dalam tiga panggilan API: generate → poll → download.
curl -X POST https://yoh.app/api/v1/stories/generate \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "prompt": "A dragon teaches a young knight how to fly", "aspectRatio": "16:9" }'{
"jobId": "cmnk8wu2q0001q3vpmqv93nmy",
"status": "queued",
"pollUrl": "/api/v1/stories/jobs/cmnk8wu2q0001q3vpmqv93nmy"
}curl https://yoh.app/api/v1/stories/jobs/cmnk8wu2q0001q3vpmqv93nmy \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"{
"status": "completed", "progress": 100,
"storyId": "cmnk8xxnx000004lbd7siief7",
"videoUrl": "https://cdn.example.com/video/clip.mp4",
"error": null
}Selesai. videoUrl adalah URL yang dapat diakses publik dan bisa langsung Anda unduh, streaming, atau bagikan.
Autentikasi
Setiap permintaan API harus menyertakan API key Anda di header Authorization menggunakan skema Bearer.
Authorization: Bearer sk_live_YOUR_API_KEYAPI key diawali dengan sk_live_. Anda dapat membuat dan mengelola key dari API Keys page. /api-keys
Jaga kerahasiaan API key Anda. Jangan tampilkan di kode sisi klien atau repositori publik. Jika key bocor, segera cabut dari halaman API Keys.
Scope & Kredit
Setiap API key diberi satu atau beberapa scope yang mengatur endpoint mana yang dapat diakses. Pembuatan teks story menghabiskan 55 kredit per job; scope yang juga menghasilkan video klip atau render akhir (termasuk scope default, full) menagih biaya tersebut di muka sebagai tambahan. Kredit dikembalikan jika job gagal setelah semua percobaan ulang.
| Scope | Memberi akses ke |
|---|---|
| stories:generate | POST /api/v1/stories/generate, POST /stories/:id/assets, POST /stories/:id/storyboard, POST /stories/:id/videos, POST /stories/:id/render, POST /stories/:id/clips/:clipId/image, POST /stories/:id/clips/:clipId/video, POST /series-assets/ensure-character-image — and the clip edits: POST/PATCH /stories/:id/clips, PATCH/DELETE /stories/:id/clips/:clipId |
| stories:read | GET /api/v1/stories, GET /api/v1/stories/:id, GET /api/v1/stories/jobs/:id |
| assets:read | GET .../characters, .../scenes, .../items, .../clips |
| social:publish | POST /api/v1/social/publish, GET /api/v1/social/publish/:postId |
Buat Video
Masukkan job pembuatan video ke antrean
/api/v1/stories/generatescope: stories:generateMengirim prompt dan langsung mengembalikan jobId. Video dibuat secara asinkron. Poll GET /api/v1/stories/jobs/:jobId untuk memantau progres.
Isi permintaan
| Field | Tipe | Status | Deskripsi |
|---|---|---|---|
prompt | string | wajib | Deskripsi story, maksimal 5000 karakter. |
aspectRatio | string | opsional | Salah satu dari 16:9, 9:16, 1:1, 21:9. Default: 16:9. |
renderVideo | boolean | opsional | false — mengembalikan klip AI pertama (cepat, ~10 menit). true — merender semua klip menjadi satu MP4 gabungan lewat Remotion (~12–15 menit). |
scope | string | opsional | story (text only, recommended), assets (+ character/scene/item images), storyboard (+ per-clip images), videos (+ clip videos), render (+ final MP4), or full (all phases). Default: full — which charges video + render credits up front. An unrecognized value falls back to full. |
title | string | opsional | Nama tampilan untuk story. |
contentType | string | opsional | Kategori format story. Salah satu dari cinematic_story, news_analysis, persona_channel, ad_spot. Default: cinematic_story. |
locale | string | opsional | Kode bahasa, misalnya en, zh-TW. Default: en. |
seriesId | string | opsional | Lampirkan ke series yang sudah ada agar visual karakter tetap konsisten. |
globalConfig opsional
Optional nested object controlling duration, output format, and model selection — the same options the studio UI exposes. Every field has a default; pass only what you want to override. The model fields (videoModel, imageModel, modelStrategy, generationMode) may also be passed at the top level as a convenience. Unknown model ids degrade to auto rather than failing the request.
| Field | Tipe | Values | Default |
|---|---|---|---|
durationSec | number | 15–300 (total seconds) | 60 |
aspectRatio | string | 16:9 · 9:16 · 1:1 · 21:9 | 16:9 |
resolution | string | 480p · 720p · 1080p | 720p |
pace | string | slow · normal · fast | normal |
structure | string | auto · 3_act · 4_act · montage | auto |
qualityTier | string | draft · balanced · premium | balanced |
stylePreset | string | preset slug, or auto | auto |
platformTarget | string | youtube · tiktok · instagram · x | youtube |
safetyMode | string | standard · strict | standard |
seed | string | any string (shared across clips for continuity) | — |
videoModel | string | a model id, or auto | auto |
imageModel | string | a model id, or auto | auto |
modelStrategy | string | auto · fixed · cost_optimized · quality_first · hero_premium | auto |
generationMode | string | auto · basic · start_end_frame · reference | auto |
locale | string | en · zh-TW · ja · ko · th · id (also accepted top-level) | en |
contentTypeConfig opsional
Optional per-contentType creative controls. Fields are validated against the selected content type; omitted fields fall back to sensible defaults. Pass the object matching your contentType.
| contentType | Fields |
|---|---|
| cinematic_story | (no per-type fields — format craft is built in) |
| news_analysis | primaryBackgroundAssetId, primaryBackgroundName (pin a studio scene across anchor shots) |
| persona_channel | primaryBackgroundAssetId, primaryBackgroundName (pin the persona's set) |
| ad_spot | heroItemId, heroProductName, heroTagline, heroKeyBenefit (the advertised product) |
Other optional fields
| Field | Tipe | Deskripsi |
|---|---|---|
storyQualityConfig | object | { enableStyleAdaptedCharacters?: boolean, directClipImageGeneration?: boolean } — quality-rollout toggles. Omit to use pipeline defaults. |
style | object | Style-consistency overrides: cinematicStyle, era, colorPalette, lightingStyle, filmGrain, lensCharacter, materialEmphasis, styleCategory, negativeHints, stylePreset. |
seriesContinuityContext | object | Series continuity context for episodic generation. Used together with seriesId. |
Fields that aren't part of the selected contentType's schema are silently ignored — cinematic_story takes no per-type fields at all, so pass contentTypeConfig only for ad_spot, news_analysis, or persona_channel.
curl -X POST $BASE/api/v1/stories/generate \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "A 30s spot for a titanium water bottle that keeps ice for 24h",
"contentType": "ad_spot",
"scope": "full",
"renderVideo": true,
"globalConfig": {
"durationSec": 30,
"resolution": "1080p",
"modelStrategy": "hero_premium",
"generationMode": "start_end_frame"
},
"contentTypeConfig": {
"heroProductName": "Northpeak Flask",
"heroTagline": "Ice, 24 hours later.",
"heroKeyBenefit": "Vacuum-sealed titanium keeps ice frozen for a full day"
}
}'Respons 202 Accepted
{ "jobId": "cmnk8wu2q0001q3vpmqv93nmy", "status": "queued", "pollUrl": "/api/v1/stories/jobs/..." }Step-by-Step Endpoints
After generating a story with scope: "story", use these endpoints to generate storyboard images, clip videos, and the final rendered MP4 as separate steps.
/api/v1/stories/:storyId/assetsGenerate the cast and world: character portraits + 2×2 character sheets, plus scene and item images. Run this before /storyboard for the best face consistency. Skips assets that already have images.
/api/v1/stories/:storyId/storyboardGenerate storyboard grid images for all clips. Skips clips that already have images.
/api/v1/stories/:storyId/videosGenerate video for each clip. Skips clips that already have a video URL.
/api/v1/stories/:storyId/renderStitch all clip videos + music + captions into a final MP4.
One Clip at a Time
The batch endpoints above only fill in what's missing — /storyboard skips clips that already have an image and /videos skips clips that already have a video. To re-roll a shot you don't like, target it directly. These are the same operations as the editor's per-clip regenerate buttons, and they overwrite whatever that clip already has.
/api/v1/stories/:storyId/clips/:clipId/image(Re)generate the storyboard image for ONE clip. Optional body: prompt (art direction for this shot) and imageModel.
/api/v1/stories/:storyId/clips/:clipId/video(Re)generate the video for ONE clip. Optional body: generationMode (basic · start_end_frame · reference) and videoModel. Omit generationMode and the clip's own saved mode is used — the same rule the editor's single-clip panel follows.
estimatedCredits) | Scope: stories:generate# Regenerate clip 3's image, then its video — the rest of the story is untouched
curl -X POST $BASE/api/v1/stories/STORY_ID/clips/CLIP_ID/image \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"
# Poll jobId until completed
curl -X POST $BASE/api/v1/stories/STORY_ID/clips/CLIP_ID/video \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"generationMode":"start_end_frame"}'
# Poll jobId until completed → the clip's videoUrl is replacedEditing the Story
Direct the story instead of only re-rolling it: rewrite a shot, retime it, add a beat, reorder, or cut one. These are immediate (no job to poll) and cost no credits. A clip's description is the prompt the generation pipelines read — edit it, then call that clip's /image or /video to re-shoot it with the new direction.
/api/v1/stories/:storyId/clips/:clipIdEdit a clip. Send only what you want to change: title, description, duration, generationDuration, generationMode, resolution, transitionStyle, sceneId, characterIds, itemIds, dialogue, audioFx.
Media columns (imageUrl, videoUrl, generation history) are deliberately not writable — those belong to the pipelines. Use the clip's /image and /video endpoints to change them.
/api/v1/stories/:storyId/clipsInsert a clip. title and description required; place it with afterClipId or beforeClipId (omit both to append). Following clips shift down automatically. The new clip has no image or video yet.
/api/v1/stories/:storyId/clipsReorder. Body: { "clips": [{ "id": "...", "order": 0 }, …] } — send every clip with a contiguous 0-based order.
/api/v1/stories/:storyId/clips/:clipIdCut a clip; the remaining orders close up. A story must keep at least one clip.
# 1. Rewrite the direction + retime the shot (immediate, free)
curl -X PATCH $BASE/api/v1/stories/STORY_ID/clips/CLIP_ID \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"description": "The keeper climbs the stairs as the lamp flickers behind him.",
"duration": 7,
"generationMode": "reference"
}'
# 2. Re-shoot it with the new direction
curl -X POST $BASE/api/v1/stories/STORY_ID/clips/CLIP_ID/image \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"
curl -X POST $BASE/api/v1/stories/STORY_ID/clips/CLIP_ID/video \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"# Step 1: Generate story text (~1-2 min)
curl -X POST $BASE/api/v1/stories/generate \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt":"A samurai cat guards a cherry blossom tree","contentType":"cinematic_story","scope":"story"}'
# Poll jobId until completed → get storyId
# Step 2: Generate storyboard images (~3-5 min)
curl -X POST $BASE/api/v1/stories/STORY_ID/storyboard \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"
# Poll until completed
# Step 3: Generate clip videos (~1-3 min per clip)
curl -X POST $BASE/api/v1/stories/STORY_ID/videos \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"
# Poll until completed
# Step 4: Render final MP4 (~1-2 min)
curl -X POST $BASE/api/v1/stories/STORY_ID/render \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"
# Poll until completed → videoUrlPoll Status Job
Dapatkan status job
/api/v1/stories/jobs/:jobIdscope: stories:readPoll endpoint ini setelah memanggil POST /api/v1/stories/generate. Job biasanya membutuhkan 8–15 menit tergantung panjang story dan apakah renderVideo diaktifkan. Disarankan polling setiap 15–30 detik.
Siklus status job
queuedgenerating_storygenerating_assetsgenerating_videosrendering_videocompletedataufailedWhich statuses you see depends on the scope you requested — a story job goes straight from generating_story to completed. A transient failure sends the job back to queued with error set to a retry message; that is not terminal, so treat only completed and failed as final states.
Field respons
| Field | Tipe | Deskripsi |
|---|---|---|
jobId | string | Pengenal unik job. |
status | string | Status saat ini. Lihat siklus di atas. |
progress | number | Bilangan bulat 0–100 yang menunjukkan persentase penyelesaian. |
storyId | string|null | Terisi setelah pembuatan story selesai. Gunakan untuk mengambil aset. |
renderVideo | boolean | Menunjukkan apakah MP4 final gabungan diminta. |
videoUrl | string|null | URL video absolut saat status completed. Berupa MP4 gabungan (renderVideo: true) atau video AI klip pertama (renderVideo: false). |
error | string|null | Pesan error yang mudah dibaca. Terisi saat gagal — dan juga saat job sedang dicoba ulang, ketika status kembali ke queued. Selalu null saat berhasil. |
createdAt / startedAt / completedAt | string (ISO) | Stempel waktu. |
Story
Daftar story
/api/v1/storiesscope: stories:readMengembalikan daftar story Anda dengan paginasi.
Parameter query
| Param | Default | Deskripsi |
|---|---|---|
page | 1 | Nomor halaman (≥ 1). |
limit | 20 | Hasil per halaman (1–100). Capped at 100. |
status | — | Filter berdasarkan status story. |
This is the only endpoint that returns a pagination envelope alongside data:
{
"data": [ { "id": "cmnk9cibl0004q3vpijkqfwl4", "title": "...", "status": "completed" } ],
"pagination": { "page": 1, "limit": 20, "total": 137 }
}Dapatkan sebuah story
/api/v1/stories/:storyIdscope: stories:readMengembalikan metadata lengkap untuk satu story, termasuk jumlah karakter, scene, item, dan klipnya.
{
"id": "cmnk9cibl0004q3vpijkqfwl4",
"title": "A robot chef cooks in a futuristic kitchen",
"status": "completed", "aspectRatio": "9:16", "locale": "en",
"counts": { "characters": 3, "scenes": 4, "items": 6, "clips": 6 },
"createdAt": "2026-04-04T11:37:04.662Z"
}Ikhtisar aset
Setelah job selesai, Anda dapat mengambil semua aset yang dihasilkan untuk story menggunakan storyId yang dikembalikan oleh job. Semua endpoint aset memerlukan scope assets:read.
GET /api/v1/stories/:id/charactersKarakter — nama, deskripsi, peran, imageUrl, info suara
GET /api/v1/stories/:id/scenesScene — deskripsi, waktu hari, cuaca, pencahayaan, imageUrl
GET /api/v1/stories/:id/itemsProperti / item — deskripsi, signifikansi, imageUrl
GET /api/v1/stories/:id/clipsKlip — narasi, durasi, videoUrl, imageUrl, audioUrl
Karakter
Daftar karakter
/api/v1/stories/:storyId/charactersscope: assets:read{
"data": [{
"id": "char_abc123", "name": "Chef Axon", "role": "protagonist",
"description": "A robot chef with a warm personality",
"imageUrl": "https://cdn.example.com/characters/chef-axon.jpg",
"voiceName": "Neutral-EN", "order": 0
}]
}Klip & Video
Daftar klip
/api/v1/stories/:storyId/clipsscope: assets:readSetiap klip adalah segmen scene dengan video hasil AI, gambar diam, dan audionya sendiri. Semua field URL bersifat absolut dan dapat diakses publik.
{
"data": [{
"id": "clip_001", "order": 0, "narration": "In the year 2157...",
"duration": 8,
"videoUrl": "https://cdn.example.com/generated/video/clip1.mp4",
"imageUrl": "https://cdn.example.com/generated/image/clip1.jpg",
"audioUrl": null, "status": "completed"
}]
}Ketika renderVideo bernilai false, videoUrl pada job mengarah ke video klip pertama. Gunakan endpoint ini untuk mendapatkan URL video dari setiap klip secara individual.
Aset Series
Daftarkan gambar karakter untuk sebuah series
/api/v1/series-assets/ensure-character-imagescope: stories:generateMengunggah dan menyimpan gambar referensi karakter untuk sebuah series agar AI menggunakan wajah/tampilan yang konsisten di semua story dalam series tersebut. Operasi ini bersifat idempotent — memanggilnya berkali-kali dengan nama karakter yang sama aman dilakukan.
| Field | Tipe | Deskripsi |
|---|---|---|
seriesId | string | ID series tempat karakter akan dilampirkan. |
characterName | string | Nama karakter (digunakan sebagai kunci unik dalam series). |
imageUrl | string | URL gambar referensi karakter yang dapat diakses publik. |
Kelola API Key
Endpoint pengelolaan API key menggunakan session authentication (cookie pengguna yang sudah login), bukan API key. Gunakan API Keys page untuk alur kerja berbasis browser, atau panggil endpoint ini secara programatis dari backend Anda dengan sesi yang valid. /api-keys
Daftar API key
/api/v1/api-keysscope: session{
"data": [{
"id": "key_abc123", "name": "Production",
"keyPrefix": "sk_live_7WH3",
"scopes": ["stories:generate", "stories:read", "assets:read"],
"revokedAt": null, "createdAt": "2026-04-03T10:00:00.000Z"
}]
}Buat API key
/api/v1/api-keysscope: session// Response — full key shown once
{
"id": "key_abc123",
"key": "sk_live_...",
"name": "My integration",
"keyPrefix": "sk_live_7WH3",
"scopes": ["stories:generate", "stories:read", "assets:read"]
}Cabut API key
/api/v1/api-keys/:idscope: session{ "id": "key_abc123", "revoked": true }Error
Semua error mengembalikan bentuk JSON yang konsisten dengan objek error berisi code yang dapat dibaca mesin.
{ "error": { "code": "insufficient_credits", "message": "...", "status": 402 } }| HTTP | code | Arti |
|---|---|---|
| 401 | unauthorized | API key hilang atau tidak valid. |
| 403 | forbidden | API key tidak memiliki scope yang diperlukan. |
| 404 | not_found | Resource yang diminta tidak ada. |
| 400 | bad_request | Isi permintaan atau parameter tidak valid. |
| 402 | insufficient_credits | Kredit tidak cukup untuk melakukan operasi ini. |
| 429 | rate_limited | Terlalu banyak permintaan. Periksa header Retry-After. |
| 500 | internal_error | Terjadi kesalahan server yang tidak terduga. |
Batas laju: Endpoint pembuatan mengizinkan 5 permintaan per menit per API key. Pembacaan dan pengeditan klip (yang tidak menghabiskan kredit) mengizinkan 60. Saat Anda mencapai batas, respons menyertakan header Retry-After berisi jumlah detik untuk menunggu. Job yang gagal (setelah semua percobaan ulang) otomatis mengembalikan kredit yang telah dipotong.
Publish to Social
Push a finished video straight to a connected account, so an automation can drive generate → render → publish with one credential. Connect the accounts in the app first; this endpoint publishes to them.
Publish a video
/api/v1/social/publishscope: social:publishReturns
202with{ postId, status: "publishing" }and uploads in the background — poll the status endpoint below. Passwait: trueto block until the upload finishes instead (can take minutes).socialAccountIdstringstoryIdstringvideoUrlstringplatformstringcaptionstringyoutubeobjectdurationSecnumberaspectRatiostringfileSizeBytesnumberwaitbooleanPoll publish status
/api/v1/social/publish/:postIdscope: social:publishLifecycle:
publishing→published|failed.