yoh
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 /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:readGET /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:writeThe 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:readGET .../characters, .../scenes, .../items, .../clips, GET /api/v1/series/:id/assets
assets:writePOST /api/v1/series/:id/assets, PATCH/DELETE /api/v1/series/:id/assets/:assetId, POST /api/v1/uploads
series:readGET /api/v1/series, GET /api/v1/series/:id
series:writePOST /api/v1/series, PATCH /api/v1/series/:id
social:publishPOST /api/v1/social/publish, GET /api/v1/social/publish/:postId
accounts:manageGET/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

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.
assetsarrayopsionalThe cast and props for this story — reference assets already in your series library, or attach one-off assets inline. See the table below.
continuityModestringopsionalWith 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.
isFinalebooleanopsionalImplied by continuityMode: "finale"; you rarely need both.
allowMultiStorybooleanopsionalLet 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.
seriesContinuityContextobjectopsionalEscape 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.

FieldTipeDeskripsi
seriesAssetIdstringReference an existing library asset. Everything else is optional when this is set.
typestringcharacter · scene · item · reference. Required for an inline asset.
namestringRequired for an inline asset. Also how the writer refers to it — name it in your prompt for it to appear.
descriptionstringOverrides the library description for this story only.
imageUrlstringImage for an inline asset. Re-hosted on our storage.
itemTypestringItems only: prop · product · logo. A logo is composited flat, never given 3D depth.
isHeroProductbooleanad_spot: the advertised product — appears in every clip. Fills heroItemId for you.
isPrimaryBackgroundbooleannews_analysis / persona_channel: the set reused across every shot. Fills primaryBackgroundAssetId for you.
useAsReferenceFramebooleanUse this image as the first frame / reference for image-to-video on clips featuring the asset.
genderstringCharacters only: surfaced to the writer.
agestringCharacters only: surfaced to the writer.
personalityTraitsstring[]Characters only: surfaced to the writer.
timeOfDaystringScenes only, e.g. dusk.
weatherstringScenes only, e.g. rain.
persistbooleanAccrue 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.

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 · premium (premium selects the advanced model lane)balanced
stylePresetstringpreset slug, or autoauto
platformTargetstringADVISORY — shown to the writer as context; nothing enforces it. youtube · tiktok · instagram · xyoutube
safetyModestringADVISORY — shown to the writer as context; not a content filter. standard · strictstandard
seedstringany string — shared across clips for continuity (API-only)
videoModelstringa model id, or autoauto
videoEditModelstringvideo editing only — muapi/sd2-video-edit, or auto. Omit to keep the existing editing recipe.auto
imageModelstringstoryboard frames + scene/item images — a model id, or auto (GPT Image 2)auto
characterImageModelstringcharacter portraits, sheets and appearance variants — a model id, or auto (Nano Banana 2)auto
modelStrategystringauto · fixed · cost_optimized · quality_first · hero_premiumauto
generationModestringDEPRECATED — ignored. Every clip uses unified reference mode.auto
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. 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.

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"
    },
    "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: 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.

Credits: that clip's duration on the story's lane (returned as estimatedCredits) | Scope: stories:generate
POST/api/v1/stories/:storyId/clips/:clipId/regenerate

Rebuild 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).

Credits: image + video, returned as breakdown | Scope: stories:generate
POST/api/v1/stories/:storyId/clips/:clipId/grid-reference

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

Credits: one storyboard grid | Scope: stories:generate
POST/api/v1/stories/:storyId/clips-all

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

Credits: images + videos, returned as breakdown | Scope: stories:generate
POST/api/v1/stories/:storyId/continue

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

Credits: one story continuation | Scope: stories:generate
GET/api/v1/stories/:storyId/renders

The 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).

Free | Scope: stories:read
POST/api/v1/stories/:storyId/clips/:clipId/video-edit

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

Credits: one video edit (flat, independent of duration) | 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 '{"videoModel":"bytedance/seedance-2.0"}'
# 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 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.

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

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

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
  }'

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

Cerita

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

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

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

Adegan — 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 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

POST/api/v1/series/:seriesId/assetsscope: assets:write

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

FieldTipeDeskripsi
typestringcharacter, adegan, item, atau reference.
namestringNama tampilan, sekaligus kunci yang dicocokkan pada generasi berikutnya.
imageUrlstringURL publik gambar. Sebagai alternatif kirim imageBase64.
itemTypestringKhusus properti: prop, product, atau logo. Logo disusun datar, tidak pernah diberi kedalaman 3D.
isHeroProductbooleanKhusus properti: produk yang diiklankan. Muncul di setiap klip.
isPrimaryBackgroundbooleanKhusus 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.
ScopeMemberi 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:

1 — create the show (once)
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", ... } }
2 — upload the logo (once) and keep the id
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.
3 — generate an ad that features it (every time)
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

POST/api/v1/series-assets/ensure-character-imagescope: assets:write

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

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

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

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

Mengembalikan 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).

FieldTipeStatusDeskripsi
socialAccountIdstringwajibAkun terhubung milik pengguna kunci Anda.
storyIdstringwajibCerita milik pengguna kunci Anda.
videoUrlstringwajibURL MP4 publik — videoUrl dari pekerjaan yang selesai.
platformstringwajibfacebook · instagram · tiktok · threads · youtube
captionstringopsionalKeterangan unggahan.
youtubeobjectopsional{ title, privacyStatus, categoryId, tags } — khusus YouTube.
durationSecnumberopsionalDipakai untuk validasi platform sebelum unggah.
aspectRatiostringopsionalDipakai untuk validasi platform sebelum unggah.
fileSizeBytesnumberopsionalDipakai untuk validasi platform sebelum unggah.
waitbooleanopsionalTunggu sampai unggahan selesai. Bawaannya 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" }
  }'

Pantau status penerbitan

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

Siklus: publishingpublished | failed.

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

POST/api/v1/accountsscope: accounts:manage

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

FieldTipeDeskripsi
externalIdstringWajib. Id Anda untuk pelanggan ini — misalnya id tenant. Unik per platform.
namestringLabel opsional, untuk pelaporan Anda sendiri.

Isi saldo sebuah akun

POST/api/v1/accounts/:accountId/creditsscope: accounts:manage

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

FieldTipeDeskripsi
amountintegerWajib. Jumlah kredit yang dipindahkan. Harus positif.
idempotencyKeystringKirim id faktur atau id periode langganan Anda. Panggilan yang diulang atau dikirim ulang lalu mengembalikan transfer aslinya alih-alih mengisi akun dua kali.
descriptionstringCatatan 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:

.mcp.json
{
  "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

AlatCakupanContoh
Bacastories:readlist_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.
Buatstories:generateestimate_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.
Seriseries:read / series:writelist_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 & dialogstories:writeupdate_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.
Timelinestories:writemove_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.
Overlaystories:writeadd_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.
Gayastories:writelist_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 & setstories:writeadd_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 suarastories:writelist_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 ulangstories:writeget_revoice · set_revoice_enabled · select_revoice_take · undo_revoiceBerpindah antara pembacaan baru dan penampilan aslinya gratis dan bisa dibatalkan; kedua take tetap disimpan.
Generasistories:generateregenerate_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-write

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.