Referensi API

Referensi API

Buat video AI secara programatis. Kirim prompt, poll hingga selesai, lalu dapatkan URL video langsung — semuanya lewat REST.

URL Dasar: https://yoh.app

Contoh 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.

1. Buat video
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" }'
Respons — job dimasukkan ke antrean
{
  "jobId": "cmnk8wu2q0001q3vpmqv93nmy",
  "status": "queued",
  "pollUrl": "/api/v1/stories/jobs/cmnk8wu2q0001q3vpmqv93nmy"
}
2. Poll hingga selesai
curl https://yoh.app/api/v1/stories/jobs/cmnk8wu2q0001q3vpmqv93nmy \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
Respons — job selesai
{
  "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_KEY

API 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.

ScopeMemberi akses ke
stories:generatePOST /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:readGET /api/v1/stories, GET /api/v1/stories/:id, GET /api/v1/stories/jobs/:id
assets:readGET .../characters, .../scenes, .../items, .../clips
social:publishPOST /api/v1/social/publish, GET /api/v1/social/publish/:postId

Buat Video

Masukkan job pembuatan video ke antrean

POST/api/v1/stories/generatescope: stories:generate

Mengirim prompt dan langsung mengembalikan jobId. Video dibuat secara asinkron. Poll GET /api/v1/stories/jobs/:jobId untuk memantau progres.

Isi permintaan

FieldTipeStatusDeskripsi
promptstringwajibDeskripsi story, maksimal 5000 karakter.
aspectRatiostringopsionalSalah satu dari 16:9, 9:16, 1:1, 21:9. Default: 16:9.
renderVideobooleanopsionalfalse — mengembalikan klip AI pertama (cepat, ~10 menit). true — merender semua klip menjadi satu MP4 gabungan lewat Remotion (~12–15 menit).
scopestringopsionalstory (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.
titlestringopsionalNama tampilan untuk story.
contentTypestringopsionalKategori format story. Salah satu dari cinematic_story, news_analysis, persona_channel, ad_spot. Default: cinematic_story.
localestringopsionalKode bahasa, misalnya en, zh-TW. Default: en.
seriesIdstringopsionalLampirkan 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.

FieldTipeValuesDefault
durationSecnumber15–300 (total seconds)60
aspectRatiostring16:9 · 9:16 · 1:1 · 21:916:9
resolutionstring480p · 720p · 1080p720p
pacestringslow · normal · fastnormal
structurestringauto · 3_act · 4_act · montageauto
qualityTierstringdraft · balanced · premiumbalanced
stylePresetstringpreset slug, or autoauto
platformTargetstringyoutube · tiktok · instagram · xyoutube
safetyModestringstandard · strictstandard
seedstringany string (shared across clips for continuity)
videoModelstringa model id, or autoauto
imageModelstringa model id, or autoauto
modelStrategystringauto · fixed · cost_optimized · quality_first · hero_premiumauto
generationModestringauto · basic · start_end_frame · referenceauto
localestringen · 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.

contentTypeFields
cinematic_story(no per-type fields — format craft is built in)
news_analysisprimaryBackgroundAssetId, primaryBackgroundName (pin a studio scene across anchor shots)
persona_channelprimaryBackgroundAssetId, primaryBackgroundName (pin the persona's set)
ad_spotheroItemId, heroProductName, heroTagline, heroKeyBenefit (the advertised product)

Other optional fields

FieldTipeDeskripsi
storyQualityConfigobject{ enableStyleAdaptedCharacters?: boolean, directClipImageGeneration?: boolean } — quality-rollout toggles. Omit to use pipeline defaults.
styleobjectStyle-consistency overrides: cinematicStyle, era, colorPalette, lightingStyle, filmGrain, lensCharacter, materialEmphasis, styleCategory, negativeHints, stylePreset.
seriesContinuityContextobjectSeries 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.

Full control example
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.

POST/api/v1/stories/:storyId/assets

Generate 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.

Credits: charged per generated image | Scope: stories:generate
POST/api/v1/stories/:storyId/storyboard

Generate storyboard grid images for all clips. Skips clips that already have images.

Credits: 20 minimum, then ~3 per clip beyond that | Scope: stories:generate
POST/api/v1/stories/:storyId/videos

Generate video for each clip. Skips clips that already have a video URL.

Credits: ~96 per 8s clip (default model; scales with duration) | Scope: stories:generate
POST/api/v1/stories/:storyId/render

Stitch all clip videos + music + captions into a final MP4.

Credits: 10 | Scope: stories:generate

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.

POST/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.

Credits: one image (no pipeline base) | Scope: stories:generate
POST/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.

Credits: that clip's duration on the story's lane (returned as estimatedCredits) | Scope: stories:generate
Re-roll one shot
# 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 replaced

Editing 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.

PATCH/api/v1/stories/:storyId/clips/:clipId

Edit 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.

Credits: 0 | Scope: stories:generate
POST/api/v1/stories/:storyId/clips

Insert 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.

Credits: 0 | Scope: stories:generate
PATCH/api/v1/stories/:storyId/clips

Reorder. Body: { "clips": [{ "id": "...", "order": 0 }, …] } — send every clip with a contiguous 0-based order.

Credits: 0 | Scope: stories:generate
DELETE/api/v1/stories/:storyId/clips/:clipId

Cut a clip; the remaining orders close up. A story must keep at least one clip.

Credits: 0 | Scope: stories:generate
Rewrite a shot, then re-shoot it
# 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-by-step example
# 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 → videoUrl

Poll Status Job

Dapatkan status job

GET/api/v1/stories/jobs/:jobIdscope: stories:read

Poll 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_videocompletedataufailed

Which 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

FieldTipeDeskripsi
jobIdstringPengenal unik job.
statusstringStatus saat ini. Lihat siklus di atas.
progressnumberBilangan bulat 0–100 yang menunjukkan persentase penyelesaian.
storyIdstring|nullTerisi setelah pembuatan story selesai. Gunakan untuk mengambil aset.
renderVideobooleanMenunjukkan apakah MP4 final gabungan diminta.
videoUrlstring|nullURL video absolut saat status completed. Berupa MP4 gabungan (renderVideo: true) atau video AI klip pertama (renderVideo: false).
errorstring|nullPesan 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 / completedAtstring (ISO)Stempel waktu.

Story

Daftar story

GET/api/v1/storiesscope: stories:read

Mengembalikan daftar story Anda dengan paginasi.

Parameter query

ParamDefaultDeskripsi
page1Nomor halaman (≥ 1).
limit20Hasil per halaman (1–100). Capped at 100.
statusFilter 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

GET/api/v1/stories/:storyIdscope: stories:read

Mengembalikan 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/characters

Karakter — nama, deskripsi, peran, imageUrl, info suara

GET /api/v1/stories/:id/scenes

Scene — deskripsi, waktu hari, cuaca, pencahayaan, imageUrl

GET /api/v1/stories/:id/items

Properti / item — deskripsi, signifikansi, imageUrl

GET /api/v1/stories/:id/clips

Klip — narasi, durasi, videoUrl, imageUrl, audioUrl

Karakter

Daftar karakter

GET/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

GET/api/v1/stories/:storyId/clipsscope: assets:read

Setiap 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

POST/api/v1/series-assets/ensure-character-imagescope: stories:generate

Mengunggah 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.

FieldTipeDeskripsi
seriesIdstringID series tempat karakter akan dilampirkan.
characterNamestringNama karakter (digunakan sebagai kunci unik dalam series).
imageUrlstringURL gambar referensi karakter yang dapat diakses publik.

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

POST/api/v1/social/publishscope: social:publish

Returns 202 with { postId, status: "publishing" } and uploads in the background — poll the status endpoint below. Pass wait: true to block until the upload finishes instead (can take minutes).

FieldTipeStatusDeskripsi
socialAccountIdstringwajibA connected account owned by your key's user.
storyIdstringwajibA story owned by your key's user.
videoUrlstringwajibPublic MP4 URL — the videoUrl from a completed job.
platformstringwajibfacebook · instagram · tiktok · threads · youtube
captionstringopsionalPost caption.
youtubeobjectopsional{ title, privacyStatus, categoryId, tags } — YouTube only.
durationSecnumberopsionalUsed for pre-flight platform validation.
aspectRatiostringopsionalUsed for pre-flight platform validation.
fileSizeBytesnumberopsionalUsed for pre-flight platform validation.
waitbooleanopsionalBlock until the upload completes. Default false.
curl -X POST $BASE/api/v1/social/publish \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "socialAccountId": "acct_123",
    "storyId": "STORY_ID",
    "videoUrl": "https://cdn.example.com/video/final.mp4",
    "platform": "youtube",
    "caption": "Episode 1 is live",
    "youtube": { "title": "The Lighthouse — Ep 1", "privacyStatus": "public" }
  }'

Poll publish status

GET/api/v1/social/publish/:postIdscope: social:publish

Lifecycle: publishingpublished | failed.

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

GET/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

POST/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

DELETE/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 } }
HTTPcodeArti
401unauthorizedAPI key hilang atau tidak valid.
403forbiddenAPI key tidak memiliki scope yang diperlukan.
404not_foundResource yang diminta tidak ada.
400bad_requestIsi permintaan atau parameter tidak valid.
402insufficient_creditsKredit tidak cukup untuk melakukan operasi ini.
429rate_limitedTerlalu banyak permintaan. Periksa header Retry-After.
500internal_errorTerjadi 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.