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 /stories/:id/clips/:clipId/video-edit, POST /stories/:id/characters/:assetId/sheet, POST /stories/:id/soundtrack, POST /stories/:id/assets/:assetId/image, POST /stories/:id/clips/:clipId/regenerate, POST /stories/:id/continue, POST /stories/:id/clips-all, POST /stories/:id/clips/:clipId/grid-reference — the clip edits: POST/PATCH /stories/:id/clips, PATCH/DELETE /stories/:id/clips/:clipId — and the story-asset edits: PATCH /stories/:id/characters|scenes|items/:assetId. Also implies assets:write and series:write, so keys issued before those scopes existed keep working. |
| stories:read | GET /api/v1/stories, GET /api/v1/stories/:id, GET /api/v1/stories/jobs/:id, GET /api/v1/credits, POST /api/v1/stories/estimate, POST /api/v1/stories/analyze-prompt. Implies series:read and assets:read. |
| stories:write | The MCP server's editing tools (POST /api/mcp): clip content and dialogue, timeline moves/trims/captions, freeform overlays, cast and set edits. Spends no credits, so a key with this scope but WITHOUT stories:generate can edit a story and physically cannot pay to generate anything. Implied by stories:generate; implies assets:write. |
| assets:read | GET .../characters, .../scenes, .../items, .../clips, GET /api/v1/series/:id/assets |
| assets:write | POST /api/v1/series/:id/assets, PATCH/DELETE /api/v1/series/:id/assets/:assetId, POST /api/v1/uploads |
| series:read | GET /api/v1/series, GET /api/v1/series/:id |
| series:write | POST /api/v1/series, PATCH /api/v1/series/:id |
| social:publish | POST /api/v1/social/publish, GET /api/v1/social/publish/:postId |
| accounts:manage | GET/POST /api/v1/accounts, GET/POST /api/v1/accounts/:accountId/credits. For platforms that serve many customers: provision an account per customer and fund it from your own balance. Implied by nothing — an existing key never gains it. |
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. |
assets | array | opsional | The cast and props for this story — reference assets already in your series library, or attach one-off assets inline. See the table below. |
continuityMode | string | opsional | With a seriesId: how much of the previous episodes this one carries. episodic_continuous (default) picks up open threads, finale resolves them, one_off_episode keeps series plot out entirely. |
isFinale | boolean | opsional | Implied by continuityMode: "finale"; you rarely need both. |
allowMultiStory | boolean | opsional | Let one request split into several episodes when the prompt clearly carries more than one. Default false — N episodes means N charges, so it is opt-in. |
seriesContinuityContext | object | opsional | Escape hatch. Supplying it replaces the continuity we assemble from your series (open threads plus the last three episode recaps), so an incomplete object produces a weaker episode than sending nothing. Prefer continuityMode. |
assets[] opsional
This array ATTACHES assets to one generation — it is not where an asset is defined. It carries only what is read inline; the full asset shape (character type and role, voice, scene lighting and mood, item material and condition) lives in the series library, which accepts all of it and which the pipeline reads directly. Reference the library by id rather than restating an asset here. Each entry either references an asset in your series library by seriesAssetId (upload it once via POST /api/v1/series/:seriesId/assets and reuse it forever), or defines a one-off asset inline with type + name. The two forms mix freely in one array. Whatever you attach is described to the writer, and its image is reused instead of a new one being generated — so a character keeps their face and a product keeps its packaging.
| Field | Tipe | Deskripsi |
|---|---|---|
seriesAssetId | string | Reference an existing library asset. Everything else is optional when this is set. |
type | string | character · scene · item · reference. Required for an inline asset. |
name | string | Required for an inline asset. Also how the writer refers to it — name it in your prompt for it to appear. |
description | string | Overrides the library description for this story only. |
imageUrl | string | Image for an inline asset. Re-hosted on our storage. |
itemType | string | Items only: prop · product · logo. A logo is composited flat, never given 3D depth. |
isHeroProduct | boolean | ad_spot: the advertised product — appears in every clip. Fills heroItemId for you. |
isPrimaryBackground | boolean | news_analysis / persona_channel: the set reused across every shot. Fills primaryBackgroundAssetId for you. |
useAsReferenceFrame | boolean | Use this image as the first frame / reference for image-to-video on clips featuring the asset. |
gender | string | Characters only: surfaced to the writer. |
age | string | Characters only: surfaced to the writer. |
personalityTraits | string[] | Characters only: surfaced to the writer. |
timeOfDay | string | Scenes only, e.g. dusk. |
weather | string | Scenes only, e.g. rain. |
persist | boolean | Accrue an inline asset into the series library so later stories can reference it by id. Default false. |
Characters, scenes and items only enter the story when the prompt names them — attaching a whole series library does not drag its entire cast into every episode. The two pin flags are the exception: a pinned hero product or primary background is guaranteed regardless of whether the script says its name.
globalConfig opsional
Optional nested object controlling duration, output format, and model selection. Every field has a default; pass only what you want to override. The model fields (videoModel, imageModel, characterImageModel, modelStrategy) may also be passed at the top level as a convenience. Unknown model ids degrade to auto rather than failing the request.
This is a superset of the studio's own controls, not a mirror of them. resolution, qualityTier, modelStrategy and seed are API-only — the studio expresses the same choices through its quality picker. Two fields are advisory: they are shown to the writer as context but nothing enforces them, and they are marked below.
| 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 (premium selects the advanced model lane) | balanced |
stylePreset | string | preset slug, or auto | auto |
platformTarget | string | ADVISORY — shown to the writer as context; nothing enforces it. youtube · tiktok · instagram · x | youtube |
safetyMode | string | ADVISORY — shown to the writer as context; not a content filter. standard · strict | standard |
seed | string | any string — shared across clips for continuity (API-only) | — |
videoModel | string | a model id, or auto | auto |
videoEditModel | string | video editing only — muapi/sd2-video-edit, or auto. Omit to keep the existing editing recipe. | auto |
imageModel | string | storyboard frames + scene/item images — a model id, or auto (GPT Image 2) | auto |
characterImageModel | string | character portraits, sheets and appearance variants — a model id, or auto (Nano Banana 2) | auto |
modelStrategy | string | auto · fixed · cost_optimized · quality_first · hero_premium | auto |
generationMode | string | DEPRECATED — ignored. Every clip uses unified reference mode. | 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. The two pin ids below are usually easier to set as isHeroProduct / isPrimaryBackground on an assets[] entry — an id you write here wins if you send both.
| 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"
},
"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: videoModel. There is no mode to pick — every clip is produced in the single unified reference mode, where a multi-reference model receives one categorized bundle: the storyboard sketch and clip image, character sheets, item, background and style images, the previous clip's tail as a video reference, and the character's voice sample as an audio reference.
estimatedCredits) | Scope: stories:generate/api/v1/stories/:storyId/clips/:clipId/regenerateRebuild ONE clip end to end — a fresh storyboard image, then a fresh video from it — as a single job. The right call after changing a clip's description or cast: regenerating only the video would run it against the old image. Optional body: prompt (art direction for the new image).
breakdown | Scope: stories:generate/api/v1/stories/:storyId/clips/:clipId/grid-referenceClip authoring supports description for scene and image context, and optional motionDescription for video movement and timed beats. Video generation uses both. When motion is empty, video generation automatically creates and saves timed motion first. Send null to prepare new motion on the next video run. Draw a multi-panel continuity reference sheet for one clip. The panels are derived from the clip's time beats — an authored beat track when motionDescription carries one, otherwise the beat plan its generation length implies — so the sheet mirrors that shot rather than a fixed set of generic moments. Two panels per beat, snapped to a supported grid layout. Runs inline and returns the grid URL.
/api/v1/stories/:storyId/clips-allDraw every missing storyboard image and then generate every missing clip video, as ONE job. Running /storyboard and /videos separately lets the video pass start while images are still missing. Both phases only fill gaps, so re-running after a partial failure is safe.
breakdown | Scope: stories:generate/api/v1/stories/:storyId/continueWrite a NEW storyline branching off an existing clip — an alternative continuation the story keeps alongside the original, which is untouched. Required body: branchFromClipId. Optional prompt (where the story goes) and clipCount. TEXT only — the new clips arrive with no images or video, so drawing them is a separate, separately-priced step.
/api/v1/stories/:storyId/rendersThe story's finished videos, newest first, with latestSuccessful called out so a failed render is never mistaken for the current cut. A story keeps every render. Optional ?limit (max 50).
/api/v1/stories/:storyId/clips/:clipId/video-editRewrite a clip's existing video from an instruction — video-to-video, so subject, composition and motion carry over. Required body: prompt ("make it night and rainy", "push the camera in slowly"). Optional durationSeconds (4–15); omit it and the edit follows the source's own length. The clip must already have a video, and the result becomes a new selectable version rather than replacing the old one.
# 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 '{"videoModel":"bytedance/seedance-2.0"}'
# 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 provides scene and image context; optional motionDescription provides video choreography. Video uses both — edit the appropriate field, 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, motionDescription, duration, generationDuration, 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
}'
# 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. |
Cerita
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, adegan, 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/scenesAdegan — 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 adegan 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.
Pustaka Aset Seri
Unggah sekali, pakai selamanya
/api/v1/series/:seriesId/assetsscope: assets:writePustaka yang sama dengan yang ditulis panel aset di studio. Unggah karakter, latar, properti, atau referensi gaya, simpan id yang dikembalikan, lalu rujuk saat generasi dengan mengirim id tersebut sebagai seriesAssetId di dalam array assets — tanpa perlu membuat tautan gambar baru untuk setiap video. Gambar yang Anda kirim di-host ulang di penyimpanan kami, sehingga id tetap berfungsi meski URL unggahan Anda sendiri kedaluwarsa.
| Field | Tipe | Deskripsi |
|---|---|---|
type | string | character, adegan, item, atau reference. |
name | string | Nama tampilan, sekaligus kunci yang dicocokkan pada generasi berikutnya. |
imageUrl | string | URL publik gambar. Sebagai alternatif kirim imageBase64. |
itemType | string | Khusus properti: prop, product, atau logo. Logo disusun datar, tidak pernah diberi kedalaman 3D. |
isHeroProduct | boolean | Khusus properti: produk yang diiklankan. Muncul di setiap klip. |
isPrimaryBackground | boolean | Khusus adegan: studio/set yang dipakai ulang di semua bidikan pembawa acara. |
- Kirim satu aset, atau array "assets" untuk menyimpan hingga 100 sekaligus.
- Secara bawaan aset digabung saat namanya sama persis, jadi mengunggah ulang katalog yang sama bersifat idempoten. Gunakan ?mergeStrategy=fuzzy untuk pencocokan yang toleran terhadap penggantian nama, atau insert untuk selalu membuat aset baru.
- Setel isHeroProduct pada properti (ad_spot) atau isPrimaryBackground pada adegan (news_analysis / persona_channel) untuk menyematkannya. Saat generasi, sematan diterjemahkan otomatis — Anda tidak perlu menulis heroItemId sendiri.
| Scope | Memberi akses ke |
|---|---|
GET | /api/v1/series |
POST | /api/v1/series |
GET | /api/v1/series/:seriesId |
PATCH | /api/v1/series/:seriesId |
GET | /api/v1/series/:seriesId/assets?type=character |
POST | /api/v1/series/:seriesId/assets |
PATCH | /api/v1/series/:seriesId/assets/:assetId |
DELETE | /api/v1/series/:seriesId/assets/:assetId |
POST | /api/v1/uploads |
PATCH | /api/v1/stories/:storyId/characters/:assetId |
PATCH | /api/v1/stories/:storyId/scenes/:assetId |
PATCH | /api/v1/stories/:storyId/items/:assetId |
POST | /api/v1/stories/estimate |
GET | /api/v1/credits |
End to end — put a brand's logo in an ad, and keep the id for every ad after it:
curl -X POST https://yoh.app/api/v1/series \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"Acme brand films","styleGuide":{"cinematicStyle":"clean product cinematography"}}'
# → { "series": { "id": "series_abc", ... } }curl -X POST https://yoh.app/api/v1/series/series_abc/assets \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "item",
"name": "Acme Logo",
"itemType": "logo",
"description": "Wordmark, orange on white",
"imageUrl": "https://your-admin.example.com/tmp/logo.png"
}'
# → { "asset": { "id": "series-item-...", "imageUrl": "https://...blob..." } }
# That imageUrl is ours now — your temporary link can expire.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 30-second spot for the Acme cordless drill",
"contentType": "ad_spot",
"seriesId": "series_abc",
"assets": [
{ "seriesAssetId": "series-item-...", "isHeroProduct": true }
]
}'
# → { "jobId": "...", "pollUrl": "/api/v1/stories/jobs/..." }Step 2 happens once per asset, not once per video. Step 3 repeats forever with the same id — and because the asset is pinned, the logo is guaranteed in every clip even if the script never says its name. To check the cost before committing, POST the same body to /api/v1/stories/estimate.
Aset Series
Daftarkan gambar karakter untuk sebuah series
/api/v1/series-assets/ensure-character-imagescope: assets:writePOST /api/v1/series-assets/ensure-character-image masih berfungsi dan tidak berubah, tetapi hanya menangani karakter. Gunakan endpoint ini.
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.
| 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. |
Akun Terkelola
Untuk platform yang melayani banyak pelanggan. Alih-alih satu kunci dan satu kolam kredit untuk semua orang — di mana satu pelanggan yang sibuk menghabiskan saldo milik yang lain dan tak seorang pun tahu siapa memakai berapa — sediakan satu akun per pelanggan dan isi masing-masing dari saldo Anda sendiri. Pelanggan Anda tidak pernah mendaftar di sini atau membayar kami; hubungan komersialnya tetap milik Anda.
Sediakan sebuah akun
/api/v1/accountsscope: accounts:manageexternalId adalah id Anda sendiri untuk pelanggan tersebut. Memanggil dua kali dengan id yang sama mengembalikan akun yang sudah ada alih-alih membuat yang kedua, jadi permintaan yang diulang tetap aman. Kunci API dikembalikan sekali saja, saat pertama dibuat — kami menyimpan hash-nya dan tidak bisa menampilkannya lagi, jadi simpanlah begitu Anda menerimanya. Akun baru dimulai dari saldo nol.
| Field | Tipe | Deskripsi |
|---|---|---|
externalId | string | Wajib. Id Anda untuk pelanggan ini — misalnya id tenant. Unik per platform. |
name | string | Label opsional, untuk pelaporan Anda sendiri. |
Isi saldo sebuah akun
/api/v1/accounts/:accountId/creditsscope: accounts:manageMemindahkan kredit dari saldo Anda sendiri ke saldo akun tersebut — sebuah transfer, bukan hibah, jadi Anda hanya bisa membagikan apa yang benar-benar Anda beli. Mengembalikan 402 bila saldo Anda sendiri tidak mencukupi. :accountId menerima id akun kami maupun externalId Anda.
| Field | Tipe | Deskripsi |
|---|---|---|
amount | integer | Wajib. Jumlah kredit yang dipindahkan. Harus positif. |
idempotencyKey | string | Kirim id faktur atau id periode langganan Anda. Panggilan yang diulang atau dikirim ulang lalu mengembalikan transfer aslinya alih-alih mengisi akun dua kali. |
description | string | Catatan opsional, tampil di kedua sisi transfer dalam riwayat kredit. |
GET /api/v1/accounts menampilkan akun-akun Anda beserta saldo dan total pemakaiannya; GET /api/v1/accounts/:accountId/credits membaca satu akun. Kunci milik akun terkelola hanya membawa cakupan generasi — ia tidak akan pernah bisa menyediakan atau mengisi akun.
Claude Code (MCP)
Yoh menyediakan server MCP sehingga agen coding AI — Claude Code, Claude Desktop, atau klien MCP mana pun — dapat membuka dan menyunting cerita Anda secara langsung: isi klip dan dialog, pemindahan dan pemotongan timeline, overlay bebas, serta regenerasi dan render setiap kali Anda menyetujuinya secara eksplisit. Server ini mengikuti aturan yang sama dengan editor di browser, jadi suntingan dari terminal mendarat persis di tempat yang sama seperti jika Anda menyeretnya.
Hubungkan
Buat API key di /api-keys — untuk menyunting tanpa memakai kredit, cakupan stories:read dan stories:write sudah cukup — lalu jalankan:
claude mcp add --transport http yoh https://yoh.app/api/mcp \
--header "Authorization: Bearer sk_live_YOUR_API_KEY"Atau commit konfigurasi proyek agar satu tim langsung mendapat koneksinya saat membuka repo, dengan masing-masing menyediakan kunci sendiri lewat env:
{
"mcpServers": {
"yoh": {
"type": "http",
"url": "https://yoh.app/api/mcp",
"headers": { "Authorization": "Bearer ${YOH_API_KEY}" },
"timeout": 900000
}
}
}Setelah itu cukup bicara ke Claude Code: "rapatkan tempo episode 3 — potong bagian kosong, geser teks agar pas di ketukan, lalu render ulang". Jalankan /mcp di dalam Claude Code untuk memeriksa koneksinya.
Apa yang bisa dilakukan agen
| Alat | Cakupan | Contoh |
|---|---|---|
| Baca | stories:read | list_stories · get_story · get_timeline · get_clip · get_job · get_credits · get_asset_images · get_soundtrack · list_overlays · get_revoice · list_style_presets · list_voices · list_renders · list_audio_assetsget_clip menyertakan gambar storyboard dan, dengan withFrames, frame nyata yang diambil dari video hasil generasi — satu-satunya cara agen bisa melihat wujud sebenarnya sebuah klip. get_asset_images melakukan hal yang sama untuk pemeran, set, dan properti. |
| Buat ★ | stories:generate | estimate_story · create_storyMenulis cerita baru dari sebuah prompt. estimate_story gratis dan memberi harga satu rencana penuh di muka, yang tidak bisa dilakukan penawaran per aksi. Mulailah dari scope="story" (teks saja) alih-alih "full", yang langsung menagih biaya video. |
| Seri | series:read / series:write | list_series · get_series · create_series · update_series · list_series_assets · add_series_asset · delete_series_assetLapisan di atas satu cerita: pemeran, set, dan gaya bersama yang dibawa lintas episode. Kaitkan cerita baru lewat seriesId pada create_story untuk mewarisinya. |
| Klip & dialog | stories:write | update_clip · update_clips · set_all_clips · reorder_clips · add_clip · delete_clip · duplicate_clip · reorder_clip · set_clip_duration · select_clip_version · add_dialogue_line · update_dialogue_line · remove_dialogue_line · hide_dialogue_line · add_audio_fx · update_audio_fx · remove_audio_fx · update_story · update_caption_settingsDialog yang melebihi klipnya otomatis menambah durasi klip tersebut. Suntingan teks tidak pernah mengubah media yang sudah dihasilkan. update_clips menyunting banyak klip sekaligus, masing-masing dengan nilainya sendiri; set_all_clips menaruh satu nilai di setiap klip dan menolak teks, karena deskripsi yang sama di mana-mana membuat episodenya datar. Keduanya menolak seluruh batch bila ada satu entri yang salah. reorder_clips menetapkan seluruh urutan dalam satu panggilan. |
| Timeline | stories:write | move_clip · resize_clip · trim_clip · reset_clip_timing · close_gaps · move_caption · hide_clip_video · pin_dialogue_read · export_clip_to_freeform · export_caption_to_freeform · remove_soundtrackMemindahkan klip menggeser semua klip setelahnya; mengubah panjang mengunci titik awal, jadi menarik bagian depan berarti memotong kepala. Item cerita tidak bisa dihapus atau dipecah — sembunyikan saja, atau ekspor menjadi overlay bebas. |
| Overlay | stories:write | add_overlay · update_overlay · move_overlay · resize_overlay · split_overlay · duplicate_overlay · delete_overlay · delete_freeform_row · upload_mediaDua overlay tidak boleh bertumpuk di satu baris; penempatan yang bentrok ditolak disertai daftar baris yang kosong. Media memerlukan sumber https yang sudah dihosting. |
| Gaya | stories:write | list_style_presets · apply_overlay_preset · set_caption_stylePreset teks, templat takarir, dan sekitar 32 animasi masuk/keluar milik editor sendiri — supaya agen memakai apa yang memang ada di produk alih-alih mengarang CSS. Gaya takarir diatur per baris dialog. |
| Pemeran & set | stories:write | add_character · update_character · delete_character · add_scene · update_scene · delete_scene · add_item · update_item · delete_itemMenghapus karakter memperbaiki semua rujukannya — pemeranan klip beserta baris dialognya, yang berpindah ke karakter bernama sama yang tersisa atau ke narator. |
| Pemilihan suara | stories:write | list_voices · set_character_voiceSuara bawaan yang disaring menurut bahasa cerita, ditambah suara kloning yang sudah siap di akun Anda. Pemilihan suara gratis; dialog yang sudah dibawakan tetap memakai suara saat direkam. |
| Rekam ulang | stories:write | get_revoice · set_revoice_enabled · select_revoice_take · undo_revoiceBerpindah antara pembacaan baru dan penampilan aslinya gratis dan bisa dibatalkan; kedua take tetap disimpan. |
| Generasi ★ | stories:generate | regenerate_clip · regenerate_clip_image · edit_clip_image · enhance_clip_image · generate_clip_video · edit_clip_video · revoice_clip · regenerate_character_sheet · regenerate_asset_image · generate_all_asset_images · generate_soundtrack · generate_storyboard · generate_all_videos · generate_all_clips · generate_clip_grid_reference · continue_story_from_clip · render_storyTiap pasangan adalah sunting-versus-gambar-ulang: edit_clip_image menulis ulang frame saat ini dari sebuah instruksi sementara regenerate_clip_image menggambar yang baru; edit_clip_video bekerja video-ke-video dan mempertahankan subjek, komposisi, serta gerak, sedangkan generate_clip_video membuat ulang dari gambar storyboard. enhance_clip_image mengembalikan kemiripan wajah berdasarkan lembar pemeran. regenerate_clip menjalankan gambar-lalu-video sebagai satu pekerjaan — pilihan tepat setelah mengubah deskripsi atau pemeran sebuah klip. generate_clip_grid_reference menggambar lembar kontinuitas yang panelnya berasal dari ketukan waktu klip itu sendiri, bukan dari kumpulan momen generik yang tetap. |
Pengeluaran dijaga dua lapis Alat bertanda ★ tidak pernah memakai kredit pada panggilan pertama: ia mengembalikan penawaran kredit dan confirmToken bertanda tangan, dan agen harus menunjukkan penawaran itu kepada Anda lalu hanya melanjutkan setelah Anda menyetujuinya secara eksplisit. Selain itu, kunci tanpa stories:generate secara teknis tidak bisa memakai kredit sama sekali — buat kunci khusus sunting dengan stories:write jika Anda ingin agen yang hanya membaca dan menyunting.
Bagaimana ia tetap aman dan benar
- Server menegakkan aturan editor (pemindahan slide, batas pemotongan, jalur teks, baris overlay) dan menjawab pelanggaran dengan rentang yang diizinkan, bukan diam-diam membatasi nilainya.
- Agen membaca seluruh aturan dari resource yoh://guide/timeline, dan prompt /mcp__yoh__edit-story memberinya alur kerja yang aman.
- Generasi bersifat asinkron — alat mengembalikan jobId yang di-poll agen dengan get_job, sama seperti REST API.
- Atur timeout panjang di .mcp.json (misalnya 900000) agar render tidak kehabisan waktu di sisi klien.
Verifikasi koneksi Anda
Repo menyertakan smoke test. Ia berbicara dengan protokol asli, menguji aturannya, dan membuktikan gerbang pengeluaran benar-benar bekerja — tanpa memakai kredit sama sekali, dan satu-satunya penulisan yang dilakukannya dikembalikan:
node scripts/mcp-smoke.mjs --url https://yoh.app/api/mcp --key sk_live_YOUR_API_KEY
# read-only variant:
node scripts/mcp-smoke.mjs --url https://yoh.app/api/mcp --key sk_live_YOUR_API_KEY --skip-writeKelola 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.
Terbitkan ke Media Sosial
Kirim video yang sudah jadi langsung ke akun yang terhubung, sehingga satu otomatisasi bisa menjalankan generate → render → terbit dengan satu kredensial. Hubungkan dulu akunnya di aplikasi; endpoint ini menerbitkan ke akun tersebut.
Terbitkan sebuah video
/api/v1/social/publishscope: social:publishMengembalikan 202 beserta { postId, status: "publishing" } dan mengunggah di latar belakang — pantau endpoint status di bawah. Kirim wait: true bila Anda justru ingin menunggu sampai unggahan selesai (bisa memakan waktu beberapa menit).
socialAccountIdstringstoryIdstringvideoUrlstringplatformstringcaptionstringyoutubeobjectdurationSecnumberaspectRatiostringfileSizeBytesnumberwaitbooleanPantau status penerbitan
/api/v1/social/publish/:postIdscope: social:publishSiklus:
publishing→published|failed.