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://www.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.

Sign in and create a key at API Keys. Choose stories:generate and stories:read for generation and polling, then copy the full key immediately — it is shown only once. Keep it in a server environment variable or secret manager. Your account needs enough credits; creating a key does not add credits.

Periksa kunci dan saldo (gratis)
curl https://www.yoh.app/api/v1/credits \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Submitting a generation request authorizes its credit charge; API integrations do not need an interactive price confirmation. To check cost first, POST the same body to /api/v1/stories/estimate. Optionally send X-Max-Generation-Credits with your maximum whole-number credits: exceeding it returns HTTP 409 before charging. Use the canonical host shown here; redirects from yoh.app can drop your Authorization header.

1. Buat video
curl -X POST https://www.yoh.app/api/v1/stories/generate \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: YOUR_UNIQUE_REQUEST_ID" \
  -d '{ "prompt": "A dragon teaches a young knight how to fly", "contentType": "cinematic_story", "aspectRatio": "16:9", "scope": "full", "renderVideo": true }'

Replace YOUR_UNIQUE_REQUEST_ID with a unique ID for this operation. Reuse that ID and the identical body when retrying a queued generation request after a network failure. A new ID starts a new operation. Direct image/music requests may return a conflict on replay; inspect the asset before retrying. Poll the returned job every 20 seconds until completed or failed. A failed job is terminal; an error on a queued job can describe an automatic retry. For a text-only draft, use scope: "story" and omit renderVideo.

Respons — job dimasukkan ke antrean
{
  "jobId": "cmnk8wu2q0001q3vpmqv93nmy",
  "status": "queued",
  "pollUrl": "/api/v1/stories/jobs/cmnk8wu2q0001q3vpmqv93nmy"
}
2. Poll hingga selesai
curl https://www.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. /settings/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 — 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, GET /api/v1/models (the models and visual styles the pickers offer, with durations, resolutions, aspect ratios, reference limits, price hints and retired ids), POST /api/v1/stories/estimate, POST /api/v1/stories/analyze-prompt. Implies series:read and assets:read.
stories:writeThe clip edits: POST/PATCH /stories/:id/clips, PATCH/DELETE /stories/:id/clips/:clipId, POST /stories/:id/clips/:clipId/duplicate; a hired character's looks: POST /stories/:id/characters/:assetId/hire-looks; starting an episode from your own video: POST /api/v1/series/:id/uploads, POST /stories/:id/upload (GET with stories:read; so is GET /stories/:id/media, every file of a story); and the MCP server's editing tools (POST /api/mcp): clip content and dialogue, timeline moves/trims/captions, freeform overlays, cast and set edits. Ordinary edits spend no credits. MCP ask_assistant charges its text turn immediately and, like quoted media generation, requires stories:generate. Implied by stories:generate; implies assets:write.
assets:readGET .../characters, .../scenes, .../items, .../clips, GET .../characters/:assetId/hire-looks, 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, POST /api/v1/uploads/presign (a one-time PUT URL for an audio, video or image file)
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.
hires:readThe MCP hiring reads: hire listings, creators who take video jobs, your hires and deliveries, casting calls and applicants, open video job requests, message threads. Implied by nothing.
hires:writeThe MCP hiring actions: send, answer and cancel hires and video jobs, deliver and review work, casting calls and open video job requests, your hire listing, messages. A payment returns a checkoutUrl for a person to open; MCP never charges a saved card. Accepting a delivery or anything that releases money needs a confirmToken. Implied by nothing.
ads:readThe MCP Yoh Ads reads: shows, briefs and results. Implied by nothing.
ads:writeThe MCP Yoh Ads actions: create and edit shows, write and make episodes, pause and resume. Budgets, autopilot and Meta launches need a confirmToken. Implied by nothing.
publish:writeThe MCP publish tools: publish and unpublish an episode on Yoh (not social posting, which is social:publish). Implied by nothing.

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 20000 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) or full (text, images and clip videos; default). Use renderVideo: true with full to include the final MP4 and its render fee. Other values return 400 before charging. For assets, storyboard, videos or render on an existing story, use the separate /api/v1/stories/:storyId endpoints below.
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. ad_spot: the real place (e.g. your shop) used in the shots that name it. 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, textFamily, 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 is the studio's resolution pills (default 720p; the story keeps the value you send and each clip makes what its model can, e.g. Seedance 2.5 up to 1080p, Kling from 720p, MiniMax H3 from its 768p tier, which the app still calls 720p (it renders slightly sharper); GET /api/v1/models gives each model's resolutionLabels, what each resolution renders as). qualityTier, modelStrategy and seed are API-only. Two fields are advisory: they are shown to the writer as context but nothing enforces them, and they are marked below.

FieldTipeNilaiDefault
storyInputModestringauto · develop · follow_script (auto decides whether the prompt is an idea to develop or a finished script to follow verbatim, for every contentType; a cue marked (V.O.) or (O.S.) becomes an offscreen: true line, voiced over the video in that character's own voice)auto
durationSecnumber15–192 (total seconds; about one 8 s clip per 8 s, at least 2 — 16 → 8+8, 20 → 8+6+6; longer content is split into episodes)32 (ad_spot 15, persona_channel 30, news_analysis 45)
clipCountnumber2–24 clips sharing durationSec, 4–12 s each (16 with 4 = 4 × 4 s); a count that does not fit is refused 400. Omitted: about one 8 s clip per 8 s, or the count the prompt names ("in 4 clips")—
aspectRatiostring16:9 · 9:16 · 1:1 · 21:91:1 (9:16 for ad_spot, persona_channel, news_analysis; a series' episode shape when seriesId is given)
resolutionstring480p · 720p · 1080p720p
pacestringslow · normal · fastnormal
structurestringauto · 3_act · 4_act · montageauto
captionStylestringstandard · words — how dialogue captions show: each line whole, or word by word (TikTok style: a few words at a time, the spoken word highlighted). Change it later with MCP update_caption_settings stylewords for 9:16, else standard
qualityTierstringdraft · balanced · premium (premium selects the advanced model lane)balanced
stylePresetstringauto, or a visual style id (list_style_presets kind visual). An unknown id is refused 400; custom style words go in style.cinematicStyleauto
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 — bytedance/seedance-2.5-video-edit (Seedance 2.5 Video Edit), or auto. The old ids muapi/sd25-video-edit and muapi/sd2-video-edit are still accepted and mean the same model. Omit to keep the existing editing recipe.auto
imageModelstringclip images (the optional storyboard) + scene/item images — a model id, or auto (GPT Image 2.5)auto
characterImageModelstringcharacter portraits, sheets and appearance variants — auto (GPT Image 2.5), google/nano-banana-2, bytedance/seedream-5-pro or xai/grok-imagine-image-2.0 (list_models: character)auto
textFamilystringthe story's text models as one family — auto (Claude), claude (Opus 5.5 · Haiku 5.5), gpt (GPT-6.1 Sol · GPT-6 Luna) or grok (Grok 4.7 · Grok 4.20 Fast). The writing, rewrites, assistant turns, translations and clip prompts run on it and the quote prices it. An unknown value is refused 400. Also accepted top-level and in MCP create_story / estimate_story / update_storyauto
modelStrategystringauto · fixed · cost_optimized · quality_first · hero_premiumauto
generationModestringDEPRECATED — ignored. The selected model determines frame or reference inputs.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.

contentTypeField
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, primaryBackgroundAssetId, primaryBackgroundName (the advertised product, and optionally the real place it is sold or used). heroItemId is an item of the seriesId series: its name fills an empty heroProductName (the tagline is never invented from its description) and the item joins the story with its picture. An id that is not an item of that series answers 400 hero_item_not_found (allowed lists the series items); heroItemId without seriesId answers 400 hero_item_needs_series. With no hero given, the series item flagged isHeroProduct is used, else the series product the prompt names, else the first product. Other series products join the story only when the prompt names them or a shot shows them.

Field opsional lainnya

FieldTipeDeskripsi
storyQualityConfigobject{ characterStyle?: "match_story" | "keep_own", characterStyleOverrides?: { [character name or series asset id]: "match_story" | "keep_own" }, directClipImageGeneration?: boolean }. characterStyle (default match_story, or the series default): a cast member whose own look is another style is redrawn in the story style, or keeps its own look. Hired characters always keep their own look. enableStyleAdaptedCharacters is deprecated (false reads as keep_own).
castArtbooleanscope "story" only. Default true, as the app: once the text is written, cast art (character sheets and shared backgrounds) starts and is charged automatically. false: draw it yourself with POST /assets. Every clip video needs it (409 ASSETS_REQUIRED otherwise). scope "full" always draws it.
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.

Contoh kontrol penuh
curl -X POST https://www.yoh.app/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/..." }

Endpoint langkah demi langkah

After generating a story with scope: "story", use these endpoints to make the cast art, an optional storyboard (clip images), clip videos, and the final rendered MP4 as separate steps.

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

Generate the cast and world: character portraits + 3×3 character sheets, plus scene and item images. Required assets must be ready before /storyboard. Skips assets that already have images. Runs as the app's Make cast art: a look the series already has is reused for free, and each asset that could not be drawn is refunded on its own. Answers 202 { jobId, runId, runUrl, pollUrl, estimatedCredits }. Scene looks are always made when clips need them; appearanceVariations is deprecated and ignored.

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

Optional storyboard review: draw one opening frame for each clip that has neither an image nor a video, the same engine as the app's Generate storyboard. Clips go in story order, four at a time as ONE 2×2 grid image (up to three grids at once), drawn at the model's largest size and split into one 1080p frame per clip; a lone last clip is its own image, and a group of two or three leaves its spare panels unused. Each panel is the clip's image prompt (stillPrompt) word for word; a missing one is written first, and a clip whose prompt cannot be written is left out before any charge. Clips a hire covers, and clips that already have an image or a video, are listed in skipped with a reason (hire_blocked, has_image, has_video). An image or video that appears while the run works is kept, never overwritten; a grid that fails (or is still unfinished after ~50 min, then cancelled) is refunded and its clips are drawn one by one only while they fit the approved credits (the quote, or X-Max-Generation-Credits when sent); the rest are left undrawn and uncharged (overApprovedBudget, reason over_approved_budget). Required character sheets, backgrounds, props and selected looks must be ready first: otherwise 409 ASSETS_REQUIRED (nothing charged) with assetsNeeded (each asset, whether it needs a picture or a look, and the clips waiting on it). Uses the saved image model; an optional imageModel overrides it for this run, and explicit auto keeps automatic selection. Stops after images. Answers 202 { runId, runUrl, clipsToGenerate, imagesToGenerate, skipped, estimatedCredits }; poll GET /api/runs/:runId. To redraw one clip afterwards, use POST /clips/:clipId/image.

Credits: one image price per 2×2 grid (its model at the drawn size, dearest fallback route; a lone clip is one single-image price) | Scope: stories:generate
POST/api/v1/stories/:storyId/videos

Generate video for each clip. Skips clips that already have a video URL. Runs as the app's own Generate All: one clip at a time in story order, each continuing the previous clip's video (parallel: true runs a few at once without that reference), each clip charged, settled and refunded on its own within the batch price. Like Generate All, clipIds narrows it to those clips, and with includeExisting: true they are re-generated even when they have a video. Answers 202 { runId, runUrl, cancelUrl, clipIds, estimatedCredits }: poll GET /api/runs/:runId until it ends (result.results lists each clip); POST /api/v1/runs/:runId/cancel stops it. /clips-all takes the same options. With fixFirst: true it first fixes what stops clips (missing character sheets, backgrounds and looks in one cast-art run; the length a clip's lines need, free; videos made before their script changed, unless listed in untick) and then generates, at ONE quote built from each step's own quote: quoteOnly: true answers the plan (fixes, needsYou, readyClipIds, quote { credits, getReady, videos, hash }); start with acceptedQuoteHash; it answers 202 { status: "queued", plan, runId, castArtRunId } (runId is left out when only pictures are fixed: follow castArtRunId, and the videos start inside that run once the pictures land). What needs you is never fixed for you and its clips are skipped; a clip whose fix fails is not started or charged. Send your own Idempotency-Key, one per consent: the same key again (a retry of the same request) joins the batch it started; a new key is a new start with its own charges, so use a new one after a failed batch. MCP: generate_all_videos with fixFirst (a resent confirmToken joins the run it started; a fresh quote is a new consent).

Credits: ~164 per 8s clip at 720p (default model; ~73 at 480p; scales with duration and resolution) | Scope: stories:generate
POST/api/v1/stories/:storyId/render

Stitch all clip videos + music + captions into a final MP4. Every clip of the cut needs its video first: otherwise it answers 409 clips_missing_video with missingVideoClips (clipId, position, title, hasImage) and charges nothing. Optional timelineMode (clip or edit); omitted, it is edit when the story has saved freeform layers, Edit-only changes or exported clips, else clip. An Edit layer with no saved media file answers 400 timeline_media_unsaved, nothing charged.

Credits: 20 | Scope: stories:generate

Satu klip setiap kali

The batch endpoints above only fill in what's missing — /storyboard skips clips that already have an image or a video (and clips a hire covers) 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.

Stories run at the same time. The clip endpoints (image, video, video-edit, regenerate, and the batches /storyboard, /videos, /clips-all) start the app's own run at once, exactly as the clip panel or Generate All would, and follow the app's rule: there is no job queue. Inside a video batch clips run one at a time in story order, each continuing the previous clip's video (/storyboard draws its 2×2 grids up to three at once); "parallel": true on /videos or /clips-all trades that continuity for speed (a few clips at once, no previous-clip video; on a single-clip endpoint it is ignored). The same request again (the same Idempotency-Key, or the same body a moment later) answers the run already started instead of charging twice; send one key per consent. Over MCP the confirmToken is that key: the same token again joins the run it started, and a fresh quote is a new start. After a 422 CONTENT_FLAGGED that carries tryGrok (the stills a batch draws first were flagged), /videos, /clips-all and /clips/:clipId/regenerate take "tryGrok": true: the same run with its stills and videos on Grok, as the app's Try with Grok (not with fixFirst). While an API job works on the story (a scope: "full" generate, a render, a continuation) a clip endpoint answers 409 API_JOB_RUNNING (nothing charged, with activeJobId and activeJobScope): retry when that job finishes. The other way round, a story-wide job (render, generate on an existing story) answers 409 STORY_BUSY (nothing charged, Retry-After) while a clip run is working, whether you or the app started it (activeJobScope app_clip_video or app_clip_image, no job to poll; retry later), or another job runs on the story (with pollUrl). A clip video whose characters have no character sheet yet, or whose background has no picture yet, answers 409 ASSETS_REQUIRED (nothing charged) with missingCharacters, missingScenes and clipIds in the error: make the cast art first (POST /assets). One live video per clip: while a clip's video is still being made (a Generate in the app, another call, a run waiting on a slow provider), a new video or video edit on that clip answers 409 CLIP_VIDEO_IN_PROGRESS (nothing charged, with runId when known): poll that run, then retry. /videos and /clips-all never refuse the whole story for one clip: a clip still being made, whose lines are longer than its video, or with nothing to work from is left out and listed in skipped with its reason (in_progress with runId, dialogue_too_long with code and details such as suggestedDurationSeconds, references_required, speaker_missing (a line's speaker left the story), hire_blocked (a hire covers it), has_video, not_found), not charged; the rest run. When every named clip is left out the start answers that clip's refusal (409 DIALOGUE_LONGER_THAN_VIDEO with its numbers, DIALOGUE_SPEAKER_MISSING, or CLIP_VIDEO_IN_PROGRESS).

POST/api/v1/stories/:storyId/clips/:clipId/image

(Re)generate the image for ONE clip (one storyboard frame, drawn alone at full resolution from its image prompt). Optional body: prompt (art direction for this shot) and imageModel. Runs as the clip panel's own image run: answers 202 { runId, runUrl, estimatedCredits }; poll GET /api/runs/:runId until it ends, then read result.status and result.imageUrl. The same Idempotency-Key answers with the run it already started.

Credits: the clip panel's image quote | Scope: stories:generate
POST/api/v1/stories/:storyId/clips/:clipId/video

(Re)generate the video for ONE clip. Optional body: videoModel, and the clip panel's pills for this run: durationSeconds (snapped up to a length the model makes) and resolution (one the model makes; GET /api/v1/models). Both are saved on the clip, as the pills save them; the aspect ratio is the story's. POST .../regenerate takes the same two. An unknown videoModel is 400; a model that cannot make the story's aspect ratio, or a duration / resolution the model does not make, is 400 invalid_video_settings with allowed in the error (aspectRatio + videoModel, durationSeconds or resolution) — an explicit pick is refused, never silently changed. A clip whose lines need more seconds than the video would have answers 409 DIALOGUE_LONGER_THAN_VIDEO (nothing charged) with clipId, speechSeconds, videoSeconds and suggestedDurationSeconds in the error: send that durationSeconds, or shorten the lines when it is null. There is no mode to pick. The selected model locks the input mode: frame models use the current clip image as start and the next clip image as end; reference models receive their supported, named image, video and voice references. Missing required clip images are rejected before charging. The final clip uses start-only where supported. Runs as the clip panel's own Generate: answers 202 { runId, runUrl, estimatedCredits, durationSeconds, resolution, aspectRatio, videoModel, requested, adjustments? } (what the run makes, and what was asked); poll GET /api/runs/:runId until it ends, then read result.status. The same Idempotency-Key answers with the run it already started; while an API job works on the clip or story it answers 409 API_JOB_RUNNING.

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 clip image, then a fresh video from it — as a single run: the app's Generate All over this clip with its still drawn first, so the video never starts against the old image, and a still that does not land leaves the video unstarted (nothing charged for it). The right call after changing a clip's description or cast. Optional body: prompt (art direction for the new image), imageModel, videoModel, and the clip panel's durationSeconds / resolution pills (saved on the clip). Answers 202 { runId, runUrl, estimatedCredits, breakdown, durationSeconds, resolution, aspectRatio, videoModel, requested } (what the clip's video makes, and what was asked); poll GET /api/runs/:runId: result.results[0] is the clip, its image the still.

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. The sheet is drawn on a 2:3 canvas when it has more rows than columns (3:2 when it has more columns, square otherwise). Starts a run and answers 202 with runId and runUrl; poll GET /api/runs/:runId until status is terminal, then read result.status: completed with the grid URL in result.imageUrl (also saved on the clip), or failed with result.error (refunded). The balance is checked before the run starts: too few credits answers 402 insufficient_credits, and a start the platform refuses answers 503 workflow_start_failed with nothing charged.

Changed 2026-10-19 (breaking): this endpoint used to answer 200 { gridImageUrl, gridData, creditsCharged } with the finished grid. It now answers 202 { runId, runUrl, status: "queued", creditsQuoted, panels, imageModel } at once. Poll GET /api/runs/:runId (MCP: get_run) every 15–30 s until status is completed, failed, canceled or needs_attention; the grid is result.imageUrl. The same Idempotency-Key answers with the run it already started (deduped: true).

Credits: one grid reference image | Scope: stories:generate
POST/api/v1/stories/:storyId/assets/:assetId/image
POST/api/v1/stories/:storyId/characters/:assetId/sheet

Draw (or redraw) ONE character, scene or item from its description, or re-derive a character's 3×3 reference sheet and portrait crop from its current portrait. Characters are sheet-first: one generation produces the sheet and the portrait together. The answer is the new URLs when the run finishes within the request. A long one (a sheet-first character with a fallback chain) answers 202 { "status": "queued", "runId", "runUrl", "creditsCharged" }: poll GET /api/runs/:runId (runUrl) until status is completed or failed; on completion result.status is the HTTP status the request would have answered and result.body the same JSON (the asset, or { error } with the charge already refunded). The charge is made before the run starts and is settled or refunded by the run either way.

Credits: one image (a character bills its sheet price) | Scope: stories:generate
GET/api/v1/stories/:storyId/characters/:assetId/hire-looks
POST/api/v1/stories/:storyId/characters/:assetId/hire-looks

The looks a hired character brings from its creator: { bookingId, owner, pictures, canChoose, looks: [{ id, kind: "main" | "style" | "look", label, styleId, styleLabel, imageUrl, characterSheetUrl, chosen, imported, clipAssetId }] }. The main sheet always comes; chosen looks are in the episode as ready looks (imported) that never apply on their own: a clip uses one when it is picked for that clip. Names only until the hire is paid (pictures: false), pictures after. POST { lookIds } sets the extra looks to bring (ids from GET; the ones left out are removed) and answers { lookIds, imported, removed, staleClipIds, looks }: clips that used a removed look go back to the usual look and are marked out of date. Free (the looks are included in the character fee); the same as Add (free) / Remove on the hire page. A character that is not hired answers 404 not_hired; an unpaid hire or one without the face 409 looks_not_available. MCP: list_hire_looks, choose_hire_looks.

Credits: none | Scope: assets:read (GET), stories:write (POST)
POST/api/v1/stories/:storyId/clips-all

Generate every missing clip video, drawing first the still of each clip that has nothing else to reference (no cast, no pictured place or item), as ONE run: the app's Generate All, each such still drawn (as the clip panel's image run) just before its own clip's video. Running /storyboard and /videos separately lets the video pass start while images are still missing. It only fills gaps, so re-running after a partial failure is safe; a still that does not land leaves its clip's video unstarted (nothing charged for it). Answers 202 { runId, runUrl, cancelUrl, imagesToGenerate, videosToGenerate, breakdown }; poll GET /api/runs/:runId.

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 (4–24). The new clips and their images are made within the price; video is not, so animating them is a separate, separately-priced step. Runs as the editor's own continuation (a story run): answers 202 { jobId, runId, runUrl, pollUrl }; poll the job (or GET /api/runs/:runId) until it ends. A paid-value price above X-Max-Generation-Credits answers 409 with the quote and starts nothing.

Credits: the editor's continuation quote, settled to cost | Scope: stories:generate
POST/api/v1/stories/preview-script

Review a finished script before it is written, free: what a follow_script write (or an episode draft) keeps of it. Body { screenplay, seriesId?, storyId? } (a storyId alone reviews that draft's stored screenplay). Answers { scenes: [{ heading, clips: [{ number, seconds, lines: [{ speaker, text }], cast }] }], linesKept, linesTotal, clipCount, totalSeconds, newCast, flags }; flags: voice_only_speakers, lines_too_long, lines_missing, too_long, not_a_script, no_lines, several_speakers (3 or more on-screen speakers in a clip: a note only, kept as written). Fixes are made in the script; the AI's wording of each shot is not previewed. MCP: preview_script.

Free | Scope: stories:read
GET/api/v1/stories/:storyId/readiness

What a paid step still needs before it can run, from the same rules that step refuses with. ?target=clip_video|storyboard|cast_art|render (default clip_video), optional clipIds (comma-separated; default: the clips the step would run), videoModel, durationSeconds, storylineId, timelineMode. Answers { target, ready, items: [{ kind, subject, reason, clipIds, fix: { action, mcpTool, args, credits } }] } in the order to fix them. An item with severity: "warn" is a warning, never a blocker: clip_location, the clip's own Setting text names another place than the location it is linked to (details.place, details.scene); fix its sceneId or stillPrompt with the clip PATCH (MCP update_clip, free) or leave it; generation is not changed; a refused start (assets_required, dialogue_longer_than_video, clip_audio_review, clips_missing_video) carries the same list as error.readiness. MCP: check_readiness.

Free | Scope: stories:read
GET/api/v1/stories/:storyId/attention

Everything on the episode that failed or waits on you, failures first: { count, items: [{ kind, subject, reason, status, clipIds, href, fix: { action, mcpTool, args, credits } }] }. Kinds: cast_art · look · look_stuck · clip_video · narration_missing · version_step · upload_lines · clip_speech: what a run left failed or wrong, plus one clip_check line when Check clips' free rules find something in clips without a video (fix check_clips). What a step still needs before it runs (and the location warning) is the readiness check's, never both. clip_speech is the voice check of a clip video: it says words the script does not have (fix remove_extra_words, free, while its sound can still be separated, else new_take; altFix keep_speech) or misses a line (fix revoice); narration_missing: the clip's narration is not in its sound. Each fix names the existing tool; try_grok runs the same step on Grok, and a priced fix (credits: null) quotes before it charges. Read it after any run. MCP: get_story_attention. The voice check judges the sound the user hears: a clip whose voice is replaced by re-voiced reads is not flagged (lineCheck.clearedBy: "voice_replaced"); captions can follow what is said (source-audio actions keepWithCaptions, captionFromSpeech, scriptCaptions; the script is unchanged). A delivered clip video always matches its quote; the clip GET's delivery.deliveryMismatch, when present, lists what differs (a bug signal, settled to the real cost).

Free | Scope: stories:read
GET/api/v1/stories/:storyId/spend

Credits an episode used: { charged, refunded, used, charges, settling, steps: [{ step, charged, refunded, used }] }, steps being write · art · videos · audio · render · other. Every charge holds its maximum and settles down to the real cost, so used = charged − refunded; settling counts charges whose final can still go down. The whole story: GET /api/v1/series/:seriesId/spend, which adds episodes (most used first; each has kind: episode, version — with versionOf and locale — or archived) and storyWide (its characters and places). Credits only; owner and editors. MCP: get_story_spend.

Free | Scope: stories:read
GET/api/v1/stories/:storyId/clip-check

Check clips before generating: the free rules over each clip's information (too many actions for its seconds, the place the text names vs the linked place, clothes vs the character's look, speakers, crowds set up as one character, time of day, a peak-moment first picture, off-frame people, continuity, lines longer than the clip) and kind method (the best way to make the clip, or a setting that does not fit). Optional clipIds (comma-separated). Answers { clipIds, issues: [{ id, clipId, clipIds, kind, severity, message, fix: { type, … }, selected, suggestion?, info? }], count, userScript }. MCP: check_clips.

Free | Scope: stories:read
POST/api/v1/stories/:storyId/clip-check

The same check with the AI review: body { clipIds?, ai: true, quoteOnly?: true, language? }. quoteOnly answers ai: { status: "quote", credits, clips, batches, storyPass } (free, about 1-3 credits a clip); without it the review runs held to X-Max-Generation-Credits (send the quoted credits) and answers ai: { status: "done", quotedCredits, chargedCredits } — the charge settles to the real cost of its calls. Two or more clips add a story-logic pass over the whole episode (kind story_logic; on a user's own script these are suggestions, selected: false). MCP: check_clips with ai: true.

Credits (quoted) | Scope: stories:generate
POST/api/v1/stories/:storyId/clip-check/apply

Apply the fixes the user picked: { issueIds } or { fixes: [{ issueId?, fix }] }. Text, place, length, voice-only and method changes go through the editor's own edits (locks, hires and collaborators respected); a fix whose text changed since the check is skipped. A new look's picture is a priced next step (next.mcpTool generate_all_asset_images). Answers { applied, outcomes: [{ issueId, fixType, status, reason?, next? }], recheck }. MCP: apply_clip_check_fixes.

Free | Scope: stories:write
POST/api/v1/stories/:storyId/voice-only-speakers

A line whose speaker is a name nobody in the cast has (readiness kind dialogue_speaker, e.g. "announcer") gets a voice-only cast member with its own voice, and its lines are credited to it — what new stories get when they are written. Optional body clipIds. Answers { members: [{ id, name, created }], creditsCharged: 0 }. MCP: cast_voice_only_speakers.

Free | Scope: stories:write
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). Each render has width, height and duration (seconds; API renders), aspectRatio, the story's locale and its fileName (API renders are named <title>_<aspect>_<language>.mp4, e.g. glow-cola_9x16_zh-TW.mp4). API renders run in their own pool (2 at once per account, 3 across accounts) and never take the app's export slots; a render past the pool waits its turn (the job reads queued, phase waiting_for_render_slot).

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

Rewrite a clip's existing video — video-to-video, so subject, composition and motion carry over. Send one of: prompt (an instruction: "make it night and rainy", "push the camera in slowly"); restyleStyleId (redraw the footage in a style preset, keeping its length); or switchAsset ({ target: { kind: "character" | "scene" | "item", id }, subject: { assetId } | { description }, updateCast? }: put the story's character, place or item named by target into the footage by its picture, in place of subject — an asset of the same kind, or a description of what to replace). restyleStyleId and switchAsset cannot be combined; with either one prompt is optional. Optional resolution (480p, 720p, 1080p — the edit dialog's pill; default the clip's, then the story's, then 720p, as in the editor). The edit keeps the source clip's length (durationSeconds is accepted for old callers and does not change it). The clip must already have a video, and the result becomes a new selectable version rather than replacing the old one. A restyle includes a frame review in its price. Runs as the clip panel's own video edit: answers 202 { runId, runUrl, estimatedCredits, durationSeconds, resolution }; poll GET /api/runs/:runId until it ends, then read result.status. A switch with updateCast (default true) points the clip's cast and speakers at the target when the edit lands.

Credits: the clip panel's edit price (source and output seconds at the edit resolution); estimatedCredits is the accepted price | Scope: stories:generate
GETPOST/api/v1/stories/:storyId/clips/:clipId/version

GET lists the clip's versions as its Versions list shows them, newest first, each with kind (generate, regenerate, edit, restyle, switch, version, delivery, upload, image, hire_review), imageUrl / videoUrl, durationSeconds, active and selectable. POST { "generationId" } makes the clip show that version: its media, its delivered length (the old trim is cleared) and a timeline span to match — the clip panel's own switch. Refused with 409 and nothing changed: HIRE_REVIEW_NOT_LIVE, CASTING_STALE, VERSION_HAS_NO_MEDIA, VERSION_NOT_READY or approved_clip_locked. Series editors and a video job's creator may switch, as in the app. MCP: select_clip_version; the clip image's own versions: select_clip_image_version.

Free | Scope: stories:read (GET), stories:write (POST)
POST/api/v1/stories/:storyId/swap-asset

Put one story asset in place of another — a character, an item or a background — in one clip (scope: "clip", clipId) or in every clip that uses it (scope: "story"), keeping the script and other clips; a character's spoken lines are re-voiced in the new character's voice inside the same video step, and its price includes that part (voice: lines, credits; refunded alone when the voice cannot be recorded, and the run answers voice.status: "not_recorded"). Body: { scope, clipId?, assetType: "character" | "item" | "scene", from, to, redo: "image" | "video" | "image_video", wardrobe?, light?, resolution?, steps?, dryRun? }. resolution (480p · 720p · 1080p) sets the video steps; omitted, each clip takes its own default, as the clip panel's edit dialog opens at (the clip's resolution, then the story's, then 720p). Per clip it is the clip panel's Switch asset (.../clips/:clipId/image or .../video-edit with switchAsset), and when each lands the clip's cast and dialogue speakers name the new asset. dryRun: true answers the plan and total without charging; otherwise each clip step is charged its own quoted price as it starts (X-Max-Generation-Credits caps the whole swap) and the answer lists each step's runId (each step is the app's own run: poll GET /api/runs/:runId; an image and a video step of one clip run side by side). Its status is started, partial or failed (409): steps that did not start are in retry.steps (nothing charged for them); resend with the same Idempotency-Key, or a new request with steps: retry.steps, and only those run. A clip where a switch to that asset is already running, in the app or from an earlier call, is listed in skipped (with its runId). Clip image and video-edit also take switchAsset / restyleStyleId directly for one clip. MCP: swap_asset.

Credits: the sum of the per-clip switch prices | Scope: stories:generate
POST/api/v1/stories/:storyId/versions

Make a new version of your story in another language, aspectRatio (16:9, 9:16, 1:1, 21:9) and/or stylePreset. Every clip keeps its media; clips are cropped or fitted to the new frame and text layers re-laid out. A language change finds where each line is spoken and translates the script by itself. A format or style version is free and made at once (201). A language version first answers a quote (200, status: "quote"): translationCredits (the copy dialog's price, charged when the version is made), laterSteps (what the paid steps after it will ask), separation (confirming it separates voice and background on each clip with dialogue, free, so the new voices play over each clip's own sound) and a confirmToken; send the same body plus confirmToken to make it, once per token. Over X-Max-Generation-Credits is 409 generation_budget_exceeded, a short wallet 402 insufficient_credits; nothing is made or charged then. Every version lives in a story; placement chooses where: "version" keeps it under its episode, unnumbered and out of the episode list and AI continuity (default for a language or format change); "episode" adds it as the next episode (default for a style change or a plain copy); "own_story" starts a new story with it as episode 1, named seriesTitle (else after the episode), its characters, backgrounds and items copied into that story's library; "other_story" adds it as the next episode of another of your stories (targetSeriesId), its cast joining that story's library (what it already has is reused) and its custom voices coming along. To move an episode, copy it there and archive the original. A target story you can't add episodes to, or an archived one, is refused (target_story_forbidden, nothing made); your custom voices come along only into a story you own. Without the right to add episodes to this story, "version" and "episode" become "own_story" and placementNote says why. "series" and "standalone" still work (= episode, own_story). The answer names its placement, seriesId and episodeNumber (null for a version). Optional clipFormat ({ clipId: "crop" | "fit_blur" | "rerender" }). Stories with hired or licensed characters can't be copied (409, error code hired_episode or licensed_cast); one that uses another creator's character is 409 external_asset. MCP: create_story_version.

Credits: a language version's translation (quoted first) | Scope: stories:generate
GET/api/v1/stories/:storyId/versions

Every story made from this one, and from those (versions of versions, up to 3 levels), newest first: language, format and style versions and template recasts. Each has depth (1 = made from this story), parentStoryId, versionOf, kind (language, format, style, copy, template, template_claim), language, aspectRatio, stylePreset, a version status (done, running, attention, ready) and links. MCP: list_story_versions. GET /api/v1/stories/:storyId and the story list carry the same provenance object.

Free | Scope: stories:read
GETPOST/api/v1/stories/:storyId/version

GET lists what is left in a version, in order — prepare (automatic, free), visual (restyle / AI re-render), voice (re-voice in the new language; it includes separating voice and background on each clip, free, and its price says so in separation), framing — each with its status and any failed clips. POST { "step": "visual" | "voice" } returns the price and a hash; POST it again with quoteHash to run it. Clips that fail are refunded one by one; POST again to retry only those. "step": "all" runs everything left with one price. A clip the step can't price or run (an unreadable length, or a re-render at 21:9) is listed in skipped with its code and is not charged. POST { "step": "prepare" } starts a failed prepare step (measure + translate) again for free (202). Refusals are 409 with busy, blocked (wait for the translation), quote_changed or nothing. A charge over X-Max-Generation-Credits is 409 generation_budget_exceeded with nothing charged; the charge counts toward the key's spend.

GET /api/v1/stories/:storyId/version/lines lists every spoken line next to its original, with the seconds it takes to say and the seconds it has; PUT it with { "edits": [{ "clipId", "index", "text" }], "keep": [...], "retake": [...] } to fix lines (retake records a line again with the same words) — a voiced clip re-voices only those lines.

Credits: the quoted price | Scope: stories:read (GET), stories:generate (POST)
Buat ulang satu shot
# Regenerate clip 3's image, then its video — the rest of the story is untouched
curl -X POST https://www.yoh.app/api/v1/stories/STORY_ID/clips/CLIP_ID/image \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
# Poll GET /api/runs/RUN_ID until completed

curl -X POST https://www.yoh.app/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 GET /api/runs/RUN_ID until completed → the clip's videoUrl is replaced

Mengedit cerita

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. Access follows the editor: anyone with edit access to the story (its owner, a series collaborator with edit rights, a job's creator) may edit it with their own key; others get 404.

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

Edit a clip. Send only what you want to change: title, description, motionDescription, stillPrompt (the prompt the clip image is drawn from; null clears it; at most 4000 characters), duration, generationDuration, resolution, transitionStyle, sceneId, characterIds, itemIds, dialogue, audioFx, onScreenText (title cards, taglines and lower thirds shown over the video: [{ text, role: title|tagline|label|other, start, end, position: top|center|bottom|lower_third }]; [] clears it).

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.

It is the editor's own save: new dialogue grows the clip when the speech grew (never shrinks it) and keeps the motion's quotes in step; a changed duration is pinned and held to the story's video model (past its maximum: 400 duration_exceeds_lane with allowed: { min, max, tiers }; within range snapped up to whole seconds, with generationDuration and the timed motion beats moved with it, as the clip panel's duration pill does); the answer, and GET on a clip or the clip list, carries resolutionMade: { target, value, adjusted, reason? } (the asked resolution against what the story's model makes); a visual edit on a clip that has an image or video answers scriptEdit (that media is out of date until you regenerate). Refusals, each 409 with nothing written: CLIP_IN_HIRE (the clip is in an unfinished hire and keeps its hired character; bookingId, characterId), approved_clip_locked (an approved collaboration version), DELIVERY_EDIT_CONFLICT (a delivered take's history), CASTING_STALE (made before the casting changed), story_locked (a job or template run holds the story).

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

Insert a clip. title and description required; place it with afterClipId or beforeClipId (omit both to append). Optional: the PATCH fields, plus storylineId (a branch of this story). Following clips shift down automatically and looks keep starting at their clip. The new clip has no image or video yet. 409 CLIP_WORKSPACE: a clip-hire workspace holds only its hired clip.

Credits: 0 | Scope: stories:write
POST/api/v1/stories/:storyId/clips/:clipId/duplicate

The editor's Duplicate: a copy right after the clip with its text, still prompt, cast, items, scene, storyline, dialogue, sound cues, on-screen text and its made image/video with their versions. Later positioned clips move right (shiftedClipIds). MCP: duplicate_clip.

Credits: 0 | Scope: stories:write
GETPOST/api/v1/stories/:storyId/clips/:clipId/audio-sync

A talking clip's audio sync, the clip panel's own read and actions. After a video lands the clip plays its own sound and separating voice and background is the user's choice (separate-audio below), except three automatic cases, platform-paid and never charged (an upload never): a talking clip from a model that takes no voice sample (Grok 1.5, Kling Omni) gets each character's own read (separation.reason voice_restored), words the voice check found that the script does not have are taken out (extra_words_removed), and a clip with narration placed over it keeps its background apart, lowered under the narrator (under_narrator). GET answers separation (state: original, separated, in_progress or no_sound; neededFor; nextStep when original), audioFinishing (status: queued, running, ready, needs_review or original; stage, error, errorCode, retryable, warnings, runId), revoice and, for needs_review, a candidate. POST { "action": "retry" | "accept" | "original" | "restore" | "background", "runId"?, "volume"? }: retry resumes a failed sync, accept uses the fitted dialogue a needs_review sync prepared, original plays the video's own audio (reversible), restore goes back to the fitted dialogue and background, background sets the separated background's volume (0–1). 409 with a code when the state does not allow it (story locked, result changed, nothing to retry, re-voice running). MCP: get_audio_sync, fix_audio_sync.

Credits: 0 | Scope: stories:read (GET), stories:write (POST)
POST/api/v1/stories/:storyId/separate-audio

Separate voice and background (free) — the user's choice outside the three automatic cases above; ask first. Re-voicing a clip and changing the music under its voices need it: a re-voice of an unseparated clip answers 409 needs_separation (or, with separateFirst: true, starts the separation and answers 202 status: "separating"). Body { "clipId"? }: that clip, or without it every clip with video not yet separated (separated, separating and soundless clips are skipped). Answers started, skipped and results ([{ clipId, status: "started" | "skipped", reason? }]); each clip's separation.state (audio-sync GET) turns separated when its run ends. "Back to original sound" is audio-sync original. GET /api/v1/stories/:storyId/clips/:clipId answers the same separation and, for your own video, sourceAudio.result: whether its lines match, which line is missing, the narrator added for free, and its actions in order (keep, separate, revoice_missing, play_original). For every other clip video it answers lineCheck, the voice check of its own speech against its lines: status match · needs_review · needs_voice, reason missing_lines · extra_speech · wrong_speaker_unknown, extraSpeech (the words), missingLines, kept. Captions keep the script's words and follow measured words (at least 0.9 s on screen, script dashes not shown). MCP: clip_source_audio (action separate), separate_story_audio.

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

Reorder. Body: { "clips": [{ "id": "...", "order": 0 }, …] } — send every clip with a contiguous 0-based order. Looks (a running injury, a change of outfit) keep starting at the clip they started at.

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

Cut a clip; the remaining orders close up. A story keeps at least one clip (400 LAST_CLIP); a clip a storyline branches from stays (409 BRANCH_POINT, with its storylineId); a clip in an unfinished hire stays (409 CLIP_IN_HIRE).

Credits: 0 | Scope: stories:write
Tulis ulang shot, lalu buat ulang
# 1. Rewrite the direction + retime the shot (immediate, free)
curl -X PATCH https://www.yoh.app/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 https://www.yoh.app/api/v1/stories/STORY_ID/clips/CLIP_ID/image \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
curl -X POST https://www.yoh.app/api/v1/stories/STORY_ID/clips/CLIP_ID/video \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
Contoh langkah demi langkah
# Step 1: Generate story text (~1-2 min)
curl -X POST https://www.yoh.app/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 (optional): Review a storyboard — one frame per clip, drawn 4 clips per 2×2 grid
curl -X POST https://www.yoh.app/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 https://www.yoh.app/api/v1/stories/STORY_ID/videos \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
# Poll GET /api/runs/RUN_ID until completed

# Step 4: Render final MP4 (~1-2 min)
curl -X POST https://www.yoh.app/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.

Stop. POST /api/v1/stories/jobs/:jobId/cancel (scope stories:write) stops a job the way the app's Stop does: no further paid step starts, work already sent to the provider finishes and is kept, and everything not delivered is refunded once. It answers { outcome: "cancelled" | "stopping" | "already_finished", status }; stopping means a clip is still in flight, so poll until the job reads cancelled (a final state too). POST /api/v1/runs/:runId/cancel stops a batch run (Generate All, started in the app or by /videos, /clips-all or /regenerate; a clip-images batch such as /storyboard) with the app's own Stop: no more clips start; clips never started are never charged. A single clip run's runId (a clip video, video edit or image) cannot be stopped: it answers 409 not_cancellable, and the run finishes and settles itself (a failure is refunded). MCP: cancel_generation.

Reading a job (changed 2026-10-20). A story job that rendered nothing (no render scope, no renderVideo) answers videoUrl: null; its first clip's video is firstClipVideoUrl, never the finished film. A failed clip image job answers failure { category, reason, message } like a clip video. A failed or cancelled job whose refund has not been settled yet says settlement: "pending". Every 402 insufficient_credits carries required, available (also as balance) and topUpUrl, which opens the story's page (or Settings → Credits when there is no story) with the buy sheet ready for what is needed; after paying it comes back to the same step, ready to press again. A Generate All that runs out of credits midway stops at that clip: its result has stoppedForCredits { clipId, required, available, notStarted, topUpUrl } and the clips after it are not started (nothing charged).

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.
waitingFor / messagestring (optional)Ada saat job menunggu hasil dari penyedia (status tetap queued, error bernilai null): nilainya "provider" dan message menjelaskan apa yang ditunggu. Terus lakukan polling.
executionobject (optional)Run Workflow yang menjalankan job ini: { version, runId, phase, heartbeatAt, status }. Ikuti lewat GET /api/runs/:runId (atau /stream-nya). Tidak ada pada job lama.
createdAt / startedAt / completedAtstring (ISO)Stempel waktu.
runId / runUrlstring (202 only)Ada di jawaban 202 setiap kali job dimulai (bukan di polling ini): run yang dimulai untuk job dan URL statusnya. Boleh dipakai; polling endpoint ini sudah cukup.

Cerita

Stop. POST /api/v1/stories/jobs/:jobId/cancel (scope stories:write) stops a job the way the app's Stop does: no further paid step starts, work already sent to the provider finishes and is kept, and everything not delivered is refunded once. It answers { outcome: "cancelled" | "stopping" | "already_finished", status }; stopping means a clip is still in flight, so poll until the job reads cancelled (a final state too). POST /api/v1/runs/:runId/cancel stops a batch run (Generate All, started in the app or by /videos, /clips-all or /regenerate; a clip-images batch such as /storyboard) with the app's own Stop: no more clips start; clips never started are never charged. A single clip run's runId (a clip video, video edit or image) cannot be stopped: it answers 409 not_cancellable, and the run finishes and settles itself (a failure is refunded). MCP: cancel_generation.

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

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.
seriesDuplicatestringHanya karakter, setelah bertanya ke pengguna: "new" menambah karakter kedua dengan nama yang sudah ada di serial.
  • Kirim satu aset, atau array "assets" untuk menyimpan hingga 100 sekaligus.
  • Secara bawaan aset digabung jika namanya sama persis, jadi mengunggah ulang katalog yang sama aman — kecuali karakter yang namanya sudah ada di serial: itu dijawab 409 SERIES_NAME_EXISTS dengan karakter yang ada, tanpa menyimpan apa pun. Tanyakan pengguna, lalu kirim seriesDuplicate "new" untuk menambah yang kedua, atau ?mergeStrategy=exact untuk memperbarui yang ada. ?mergeStrategy=fuzzy toleran terhadap ganti nama; insert selalu membuat aset baru.
  • DELETE menjawab 409 SERIES_ASSET_IN_USE beserta episode yang masih memakai aset (stories: id, title, clipCount) dan tidak menghapus apa pun. Tanyakan pengguna, lalu kirim confirm: true (body atau ?confirm=true): episode tetap menyimpan salinannya sendiri dan klipnya tidak berubah. Karakter dalam sewa yang masih berjalan menjawab 409 SERIES_ASSET_HIRED (hires) dan tidak bisa dihapus sampai sewanya selesai.
  • 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
DELETE/api/v1/series/:seriesId/assets/:assetId/looks/:lookId
POST/api/v1/series/:seriesId/assets/:assetId/looks/:lookId
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
GET/api/v1/models
POST/api/v1/stories/:storyId/swap-asset
POST/api/v1/series/:seriesId/uploads
POST/api/v1/stories/:storyId/upload
GET/api/v1/stories/:storyId/upload
POST/api/v1/uploads/presign
GET/api/v1/stories/:storyId/media

Start from your own video (MP4, MOV or WebM, up to 1 GB and 10 minutes; free): POST /api/v1/series/:seriesId/uploads makes a draft episode and answers a one-time upload.url that takes one PUT of the file, straight to storage (upload.command is the curl line). Then POST /api/v1/stories/:storyId/upload verifies it, fills an empty title and description from the video, sets its style, picks a cover and cuts it into editable clips; it answers verifying until the check is done, so call it again. GET /api/v1/stories/:storyId/upload reports cutting (with progress) until ready, and links.upload is the page that shows it.

Start from a video
curl -X POST https://www.yoh.app/api/v1/series/series_abc/uploads \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"fileName":"trip.mp4"}'
# → { "storyId": "story_123", "upload": { "url": "https://…", "command": "curl --fail-with-body -sS -X PUT -T <path-to-video> '…'" }, "links": { … } }

curl --fail-with-body -sS -X PUT -T ./trip.mp4 "<upload.url>"

curl -X POST https://www.yoh.app/api/v1/stories/story_123/upload \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
# → { "status": "cutting", "title": "…", "cut": { "progress": 10, … }, "links": { "upload": "…", "workspace": "…" } }

End to end — put a brand's logo in an ad, and keep the id for every ad after it:

1 — buat cerita (sekali)
curl -X POST https://www.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 — unggah logo (sekali) dan simpan id-nya
curl -X POST https://www.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 — buat iklan yang menampilkannya (setiap kali)
curl -X POST https://www.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). URL video harus cocok dengan hasil render yang berhasil atau video asli yang diunggah untuk cerita ini. Streaming YouTube hanya mengizinkan alamat publik dan mengikuti batas unggahan 512 MB yang sudah berlaku.

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 https://www.yoh.app/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: publishing → published | 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.

Ganti kunci akun terkelola

POST/api/v1/accounts/:accountId/api-keysscope: accounts:manage

Jika kunci awal hilang, panggil endpoint ini dengan kunci platform pemilik tanpa isi permintaan. Gunakan ID akun atau externalId. Dalam satu transaksi, semua kunci akun sebelumnya dicabut dan apiKey baru ditampilkan sekali (201). Simpan dengan aman. Mengulangi panggilan ini akan mengganti kunci lagi; mengulangi permintaan pembuatan akun biasa tidak mengganti kunci yang ada.

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. Coba lagi dengan kunci, penerima, dan jumlah yang sama. Percobaan ulang tidak mentransfer kredit lagi, meskipun saldo platform sudah habis. Mengubah penerima atau jumlah menghasilkan 409. Kunci yang terlalu panjang ditolak, bukan dipotong.
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 & Codex (MCP)

Yoh menyediakan server MCP sehingga agen AI — Claude Code, Codex, Claude Desktop, atau klien MCP mana pun — dapat membuat dan menyunting cerita di akun Anda: menulis cerita baru dari sebuah ide, memilih pemeran dan membuat videonya, atau mengubah isi klip dan dialog, pemindahan dan pemotongan timeline, serta overlay bebas. Apa pun yang memakai kredit selalu menunggu persetujuan eksplisit Anda. Server ini mengikuti aturan yang sama dengan editor di browser, jadi suntingan dari terminal mendarat persis di tempat yang sama seperti jika Anda menyeretnya.

Tiga cara memakainya

Pilih yang cocok dengan alat yang dimiliki agenmu. Skill yoh di bawah mengajarkan ketiganya.

  • MCP + browser (disarankan di Codex, Claude Code, dan aplikasi Claude) Agen bekerja lewat alat MCP ini dan sekaligus mengendalikan satu tab browser (Claude in Chrome, browser Codex): setelah tiap langkah ia membuka tautan dari jawabannya (tabel Naskah setelah cerita baru, panel klip setelah video klip, daftar Generate All saat proses berjalan, editor video untuk timeline dan lapisan teks, MP4 hasil render) lalu memeriksa hasilnya di halaman aslinya. Masuk sekali di browser itu dengan akun yang sama dengan kunci API. MCP dan halaman memakai data yang sama: halaman episode memperbarui dirinya beberapa detik setelah perubahan lewat MCP, dan agen memuat ulang editor video setelah perubahan timeline. Setiap pembelanjaan hanya lewat satu jalur: penawaran MCP atau dialog harga aplikasi.
  • Hanya browser (tanpa kunci API) Gunakan halaman Yoh melalui browser. Tinjau harga pembuatan di dialog konfirmasi aplikasi. Giliran teks asisten dapat langsung dikenai biaya; sepakati biaya itu sebelum menggunakannya. Jangan masukkan detail pembayaran atau kode masuk.
  • Hanya MCP (tanpa browser) Untuk skrip, CI, dan agensi: alat ★ menggunakan penawaran → confirmToken. Pantau get_job / get_run, hentikan dengan cancel_generation, lalu unduh video yang selesai. ask_assistant langsung menagih giliran teks; pembuatan terjadwal dan pembayaran eksternal memiliki aturan persetujuan tersendiri. Panggilan MCP tidak memerlukan browser.

Contoh di setiap cara: iklan 16 detik 9:16 dalam bahasa Inggris dan Jepang, ditulis dalam satu cerita bersama produknya, dibuat videonya, dijadikan versi bahasa Jepang, lalu dirender dua kali.

Hubungkan

Disarankan: masuk dengan Yoh, tanpa kunci API. Gunakan alamat ini persis (yoh.app tanpa www dialihkan, dan proses masuk memeriksa alamatnya). Di Claude Code:

claude mcp add --transport http --scope user yoh https://www.yoh.app/api/mcp

Lalu ketik /mcp, pilih yoh, pilih Authenticate, masuk ke Yoh di browser yang terbuka dengan akun yang ingin kamu pakai, lalu setujui.

Di Codex (CLI, ekstensi IDE, atau aplikasi desktop ChatGPT), tambahkan server dan masuk, lalu atur tool_timeout_sec = 300 di bawah [mcp_servers.yoh] pada ~/.codex/config.toml dan mulai ulang Codex:

codex mcp add yoh --url https://www.yoh.app/api/mcp
codex mcp login yoh
  • Aplikasi desktop Claude atau claude.ai: Customize → Connectors → + Add → Add custom connector, beri nama Yoh, tempel alamatnya, lalu Connect dan masuk. Aktifkan di setiap chat dari + → Connectors. Pada paket Team atau Enterprise, pemilik menambahkannya lebih dulu.
  • Cursor: tambahkan yoh dengan alamat ini ke ~/.cursor/mcp.json, lalu masuk saat Cursor meminta.
  • ChatGPT: Plugins → + → Add custom MCP server, beri nama Yoh, tempel alamatnya, lalu masuk. Beberapa paket memerlukan admin untuk mengizinkan server kustom.

Cek: minta agenmu menjalankan get_credits; hasilnya harus menampilkan saldo Yoh-mu. Kamu bisa melihat dan memutus aplikasi terhubung di Pengaturan → Kunci API → Aplikasi terhubung.

Agar agenmu bisa melihat halaman yang ia ubah: Claude Code dengan claude --chrome (ekstensi Claude in Chrome), aplikasi desktop Claude dengan konektor Claude in Chrome, atau Codex di aplikasi desktop ChatGPT dengan browsernya. Skill memberi tahu agen kapan harus melihat.

Atau pakai kunci API (skrip, CI, aplikasi tanpa proses masuk)

Buat API key di /settings/api-keys. Untuk membuat cerita baru dan video, sertakan stories:generate; untuk menyunting tanpa memakai kredit, stories:read dan stories:write sudah cukup. Lalu di Claude Code:

claude mcp add --transport http yoh https://www.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://www.yoh.app/api/mcp",
      "headers": { "Authorization": "Bearer ${YOH_API_KEY}" },
      "timeout": 900000
    }
  }
}

Di Codex, tambahkan server ke ~/.codex/config.toml dan atur YOH_API_KEY di shell Anda:

~/.codex/config.toml
# export YOH_API_KEY=sk_live_YOUR_API_KEY
[mcp_servers.yoh]
url = "https://www.yoh.app/api/mcp"
bearer_token_env_var = "YOH_API_KEY"
tool_timeout_sec = 300

Pasang skill Yoh

Skill ini mengajari agen Anda cara membuat dan menyunting cerita Yoh dengan baik: mulai di dalam serial, cek harga sebelum memakai kredit, dan lihat klipnya sebelum menilai. Hanya satu file, selalu mengikuti server. Jalankan baris untuk agen Anda, atau unduh SKILL.md

# Claude Code
mkdir -p ~/.claude/skills/yoh && curl -fsSL https://www.yoh.app/skills/yoh/SKILL.md -o ~/.claude/skills/yoh/SKILL.md

# Codex
mkdir -p ~/.agents/skills/yoh && curl -fsSL https://www.yoh.app/skills/yoh/SKILL.md -o ~/.agents/skills/yoh/SKILL.md

Setelah itu cukup minta: "buat cerita 60 detik tentang perawat shift malam yang bisa mendengar hantu", atau "rapatkan tempo episode 3 — potong bagian kosong, geser teks agar pas di ketukan, lalu render ulang". Jalankan /mcp di Claude Code atau Codex untuk memeriksa koneksinya.

These task guides explain which tool calls to make. They do not turn ordinary reads or edits into background jobs. Start with your task below. The connected MCP tool list provides each tool's exact arguments; use returned IDs and links. A missing scope requires a key with that permission. After a write, read the result back; a started job is not a completed video.

Turn a script or PDF into a series

Use plan_series with text, an uploaded pdfUrl, or an idea. Poll get_series_plan, review episode boundaries and cast, then create_series_from_plan. It saves draft episodes without generation. Read each draft with get_story before quoting its generation steps. bulk_create_episodes is for writing new episodes, not filling those drafts.

Write an episode from a finished script

Call preview_script first (free) and show the user linesKept of linesTotal, clipCount, totalSeconds and its flags; fixes are made in the script. Then quote create_story with storyInputMode follow_script and the script's locale (or storyDraftId for a saved draft); the quote prices the script's own clips and length. A followed script starts no cast art: quote generate_all_asset_images next.

Fix what blocks generation, then generate

check_readiness (free; target clip_video, storyboard, cast_art or render) lists each missing thing with its fix tool, arguments and price; fix them in order and check again. Or quote generate_all_videos with fixFirst: true for one price that fixes and generates; plan.needsYou lists what only the user can fix, and untick leaves a fix out. After any run, read get_story_attention (free) and tell the user each failure and its fix; a try_grok fix re-runs the step on Grok after a content flag, with the user's OK. Over MCP a refusal's extra fields (readiness, tryGrok, suggestedDurationSeconds) sit on the error itself. A tryGrok offer is taken by quoting the same tool again with tryGrok: true (create_story, generate_all_asset_images, generate_storyboard, generate_all_clips, generate_all_videos, regenerate_clip, generate_clip_video) and confirming that new quote. The fixFirst confirm answers runId and castArtRunId; with only picture fixes there is no runId, so poll castArtRunId. Generate All leaves out a clip for its own reason (skipped: in_progress, dialogue_too_long, references_required, speaker_missing, hire_blocked, has_video, not_found) and runs the rest.

Retries, replays and refusals over MCP

The confirmToken is the idempotency key: sending the same token again joins the run it started and is never charged twice; a new quote is a new start, so quote again after a failed batch. While the first confirm is still starting, a resend answers confirmation_in_progress with retryAfterSeconds: send the same token again. confirmation_needs_review means that confirm started work and then failed: read get_job or get_run, then quote again. confirmation_inputs_changed and confirmation_stale need a fresh quote. A refusal that started nothing (a busy story, a rate limit, credits) frees the token for a retry. v1 refusals reach MCP with their own codes: GENERATION_PRICE_CONFIRMATION_REQUIRED (quote again), WORKFLOW_START_FAILED (nothing charged; retry), INSUFFICIENT_CREDITS (required and available), GENERATION_IN_PROGRESS (poll its runId). A clip with a Motion reference video takes the footage's length (footage_sets_length): pick a longer range with set_clip_motion_reference instead of set_clip_duration. The full table with each next step is in the guide resource yoh://guide/timeline and the skill.

Edit existing pictures, motion or audio

Read get_clip, list_asset_image_versions, list_looks and get_source_audio first. Use set_clip_image or use_asset_image_version to reuse an existing image; edit_asset_image edits a picture and draw_asset_image draws a new one, both quoted first. An asset edit returns a preview; use_asset_image_version applies it. draw_asset_image applies by default (preview:true requests review first). Text and look suggestions must be saved separately after approval. Before re-voicing or changing the music under voices, check get_clip separation: when it is original, offer to separate voice and background (free; clip_source_audio separate, or separate_story_audio for every clip), then re-voice or adjust. Offer Separate only when get_source_audio separationAvailable is true (a clip with no sound is no_sound). Movement footage for one clip: set_clip_motion_reference (3–12 s range), set_clip_motion_source, remove_clip_motion_reference. set_voice_mix controls source speech/background tracks; set_soundtrack_playback controls music volume. Text edits do not update generated media.

Hire a creator or offer your work

Two kinds, both paid at send and refunded in full if declined or unanswered. Use a character: search_hire_listings, inspect quote_hire, then send_hire_request after terms approval (per-episode price; a posting package or bundle adds the owner posting your video). pay_hire hands checkout to the user; get_hire tracks payment and delivery. Video job: a creator makes the whole episode (never a subset of scenes): search_video_job_creators then hire_creator_clips, or post_clip_call and pick_clip_offer. The creator works in your episode with the normal tools and sends it with send_job_video; review_delivery acceptance can release payment. open_clip_workspace and send_clip_take are legacy (clip hires sent before 2026-10-17). set_character_listing creates a first listing; an uploaded photo alone does not establish likeness rights.

Build a Yoh Ads show

Create with create_brand_show, read get_brand_show and review its brief and season, then accept_brand_show. Quote write_show_episode for a planned slot. Poll get_brand_show, then quote make_show_episode for videos and again for the render. get_show_plan previews run_show_autopilot; review credit costs, posting and Meta money actions together. Follow get_show_activity. Connect Meta accounts in the app.

Publish a finished video

Inspect list_renders and get_publish_options. Confirm the selected video, audience and any template rights before publish_story. For social posting, list_social_accounts, approve account/video/caption, call social_publish, then poll get_social_post. Publishing is a separate decision from rendering.

Check spending and manage access

Use get_credits for balance, get_credit_history for charges (each with its receipt: charged, refunded, final), get_story_spend for what a story used, get_earnings for creator income and list_notifications for activity. list_api_keys shows metadata; revoke_api_key removes access. Check get_auto_generate before changing a schedule; enabling set_auto_generate needs explicit recurring-spend consent and stories:generate as well as series:write.

★ means a credit quote followed by confirmToken. Other costs need their own review: ask_assistant charges its text turn immediately and needs stories:generate, set_auto_generate enables future credit spending, and hiring or Meta ads can involve external money. No ★ does not mean free. Review those costs with the user before calling.

Apa yang bisa dilakukan agen

AlatCakupanContoh
Bacastories:readlist_stories · get_story · list_story_versions · get_timeline · get_clip_timeline · get_edit_timeline · list_timeline_versions · get_clip · get_job · get_run · get_credits · list_models · get_asset_images · get_soundtrack · list_overlays · get_revoice · get_audio_sync · list_style_presets · list_voices · list_renders · list_audio_assets · check_readiness · get_story_attention · preview_script · estimate_storyget_clip menyertakan gambar klip (jika ada) 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 karakter, set, dan properti.
Template versionsstories:write / stories:read / assets:readcreate_template_version · get_template_version · list_template_destinations · list_template_assets · update_template_version · analyze_template_version · get_template_analysis · accept_template_adaptation · quote_template_version · commit_template_version · cancel_template_version · fork_template_versionMap existing or newly created series assets, review wording and language, then create a free draft. REST families: /api/v1/template-version-sessions/:sessionId and /api/v1/template-version-runs/:runId. Template REST OpenAPI schemas and endpoints
Template generation ★stories:generategenerate_template_versionQuote the run first, show the maximum credits, then confirm. Poll get_template_version for persisted progress and recovery.
Buat ★stories:generatecreate_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.
Start from a videostories:write / stories:read / assets:writeupload_video · finish_video_upload · get_video_upload · upload_file · list_story_mediaFree. upload_video makes a draft episode in a series and answers one curl command that sends the local file straight to storage; finish_video_upload verifies it, fills the details from the video, picks a cover and cuts it into editable clips; get_video_upload reports the cut until it is ready. Open links.upload to watch it. upload_file hosts a song, logo or extra footage the same way (for an overlay src or an asset picture); list_story_media lists every file of a story to download and check.
Story versions ★stories:generate / stories:readcreate_story_version · create_story_versions · get_story_version · get_version_translation · edit_version_translation · list_story_versionsA language, format or style version keeps every clip's media. A format or style version is free; a language version's translation is charged when it is created, so it answers a quote first and is made with confirmToken (the copy dialog's price). create_story_versions makes many at once the cheap way: each new style or aspect once, then its languages from it after its visual step. create_story_version(s) need stories:generate, edit_version_translation stories:write; the reads need stories:read. The paid steps run with run_story_version_step (stories:generate); its free 'prepare' step restarts a failed translation.
Seriseries:read / series:write assets:read / assets:writelist_series · get_series · create_series · update_series · list_series_assets · add_series_asset · update_series_asset · delete_series_asset · remove_series_look · use_series_look_as_mainLapisan di atas satu cerita: karakter, latar, 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 · select_clip_image_version · add_dialogue_line · update_dialogue_line · cast_voice_only_speakers · 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.
Asisten Yohstories:writeask_assistant · apply_assistant · undo_assistantSerahkan permintaan ke asisten Yoh sendiri, yang sama dengan tanya yoh di halaman Naskah dan agen AI di editor. ask_assistant hanya mengusulkan: suntingan kembali sebagai perubahan dan langkah berbayar sebagai penawaran harga. apply_assistant menjalankan perubahan yang Anda simpan; langkah berbayar juga butuh confirmToken dari penawarannya setelah pengguna setuju. undo_assistant mengembalikan apa yang diubah oleh run yang sudah diterapkan. Setiap giliran ask_assistant memakai beberapa kredit: biaya AI sebenarnya, paling banyak sebesar kutipan kecil per giliran.
Timelinestories:writeedit_timeline · add_timeline_version · move_clip · resize_clip · trim_clip · reset_clip_timing · close_gaps · set_clip_framing · set_clip_canvas_layout · move_caption · hide_clip_video · pin_dialogue_read · export_clip_to_freeform · export_caption_to_freeform · remove_soundtrackMode Clip menampilkan struktur dan posisi asli. Mode Edit menyimpan perubahan secara terpisah. Jenis lapisannya sama, termasuk Suara dan Musik, tetapi posisi dan hasil render dapat berbeda. Baca get_edit_timeline sebelum edit_timeline; pengeditan tidak memerlukan ekspor atau Workflow. Saat merender, pilih Clip atau Edit dan tentukan timelineMode pada render_story.
Overlaystories:writeadd_overlay · update_overlay · move_overlay · resize_overlay · split_overlay · duplicate_overlay · delete_overlay · delete_freeform_row · upload_mediaAlat overlay dengan ID numerik ini mengikuti aturan lama: dua overlay tidak boleh berada di satu baris (bentrokan ditolak disertai daftar baris yang kosong) dan waktu dibulatkan ke 0,1 detik. Untuk overlay yang bertumpuk atau suntingan yang tepat per frame, gunakan get_edit_timeline + edit_timeline. Media memerlukan sumber https yang sudah dihosting.
Gayastories:writeapply_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.
karakter & setstories:writeadd_character · update_character · delete_character · add_scene · update_scene · delete_scene · add_item · update_item · delete_item · keep_character_sheetMenghapus karakter mengeluarkannya dari setiap klip. Baris dialognya tetap ada tanpa pembicara sampai Anda memilih siapa yang mengucapkannya (update_dialogue_line); peran dengan wajah berlisensi atau sewa yang masih menunggu tidak bisa dihapus.
Pemilihan suarastories:writeset_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:writeset_revoice_enabled · select_revoice_take · undo_revoice · fix_audio_syncBerpindah antara pembacaan baru dan penampilan aslinya gratis dan bisa dibatalkan; kedua take tetap disimpan.
Generasi ★stories:generateregenerate_clip · regenerate_clip_image · edit_clip_image · enhance_clip_image · generate_clip_video · edit_clip_video · swap_asset · 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 · run_story_version_step · 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 referensi klip (lembar karakter, latar, properti, dan gambarnya jika ada). enhance_clip_image mengembalikan kemiripan wajah berdasarkan lembar karakter. regenerate_clip menjalankan gambar-lalu-video sebagai satu pekerjaan — pilihan tepat setelah mengubah deskripsi atau karakter 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.
Stopstories:writecancel_generationThe app's Stop for a jobId or a batch runId: nothing further starts, work in flight finishes and is kept, the rest is refunded. Free.
Hired character looksassets:read stories:writelist_hire_looks · choose_hire_looksThe creator's looks a hired character brings (main sheet, earlier style sheets, the creator's looks) and which are in the episode; choose_hire_looks adds or removes them, the same as the hire page. Names only until the hire is paid. Free.
Plan scripts and create episodesstories:writeplan_series · create_series_from_plan · split_series_arc · cancel_bulk_jobPlan a script or PDF, poll get_series_plan, review the plan, then store draft episodes. bulk_create_episodes writes NEW episodes and cast art; it does not make videos. Poll get_bulk_job; cancel_bulk_job stops after the current episode.
Plan scripts and create episodesstories:readget_series_plan · get_bulk_jobPlan a script or PDF, poll get_series_plan, review the plan, then store draft episodes. bulk_create_episodes writes NEW episodes and cast art; it does not make videos. Poll get_bulk_job; cancel_bulk_job stops after the current episode.
Plan scripts and create episodes ★stories:generatebulk_create_episodesPlan a script or PDF, poll get_series_plan, review the plan, then store draft episodes. bulk_create_episodes writes NEW episodes and cast art; it does not make videos. Poll get_bulk_job; cancel_bulk_job stops after the current episode.
Find and install templatesstories:readlist_templates · preview_template · list_idea_templates · get_uploaded_assetsPreview a template before claiming, installing or copying it. Review detected upload assets before accepting them into the cast.
Find and install templatesstories:writeclaim_template · install_idea_template · copy_prepared_template · review_uploaded_assetPreview a template before claiming, installing or copying it. Review detected upload assets before accepting them into the cast.
Manage a series and its teamseries:writereorder_episodes · archive_series · set_auto_generate · invite_collaborator · update_collaborator_role · remove_collaborator · set_series_coverRead the saved schedule and collaborators first. Enabling set_auto_generate also needs stories:generate and consent:true: future episodes spend owner credits without another approval. Disable with enabled:false.
Manage a series and its teamstories:writearchive_storyRead the saved schedule and collaborators first. Enabling set_auto_generate also needs stories:generate and consent:true: future episodes spend owner credits without another approval. Disable with enabled:false.
Manage a series and its teamseries:readget_auto_generate · list_collaboratorsRead the saved schedule and collaborators first. Enabling set_auto_generate also needs stories:generate and consent:true: future episodes spend owner credits without another approval. Disable with enabled:false.
Motion references and clip audiostories:writeupload_clip_video · set_clip_motion_reference · remove_clip_motion_reference · set_clip_motion_source · set_clip_video · set_clip_image · clip_source_audio · separate_story_audio · set_voice_mixRead get_source_audio or get_revoice before choosing tracks and volumes. Mix existing audio for free; check_clip_reads and generate_clip_panorama quote credits first.
Motion references and clip audiostories:readget_source_audioRead get_source_audio or get_revoice before choosing tracks and volumes. Mix existing audio for free; check_clip_reads and generate_clip_panorama quote credits first.
Check clips ★stories:generatecheck_clipsFree rules without ai; ai:true adds the AI review of the clips and the story, quoted first (confirmToken). Show the findings, then apply the ones the user keeps.
Check clipsstories:writeapply_clip_check_fixesFree. Applies the picked fixes through the editor's own edits; a new look's picture is a priced next step.
Motion references and clip audio ★stories:generatecheck_clip_reads · generate_clip_panoramaRead get_source_audio or get_revoice before choosing tracks and volumes. Mix existing audio for free; check_clip_reads and generate_clip_panorama quote credits first.
Asset pictures and looksstories:readlist_asset_image_versions · get_asset_image_job · list_looksList versions and looks before choosing one. Edit an existing picture or draw a new one; both quote credits first. Poll get_asset_image_job for returned jobs.
Asset pictures and looksstories:writeuse_asset_image_version · add_look · update_look · delete_look · set_clip_look · import_lookList versions and looks before choosing one. Edit an existing picture or draw a new one; both quote credits first. Poll get_asset_image_job for returned jobs.
Asset pictures and looks ★stories:generateedit_asset_image · draw_asset_image · generate_missing_series_pictures · polish_asset_description · suggest_looksList versions and looks before choosing one. Edit an existing picture or draw a new one; both quote credits first. Poll get_asset_image_job for returned jobs.
Custom voicesstories:writecreate_custom_voice · delete_custom_voiceCreate or remove a custom voice using authorized reference audio; this does not regenerate existing dialogue.
Reuse library assetsassets:writepromote_asset_to_series · copy_series_assetsRead episode assets, promote them to the series library, or copy library assets to another series.
Reuse library assetsseries:readlist_episode_assetsRead episode assets, promote them to the series library, or copy library assets to another series.
Editor playbackstories:writeset_soundtrack_playback · set_on_screen_text_look · set_playback_speed · export_clip_audio_to_freeform · restore_clip_audioSet music playback, text appearance, speed and detached audio.
Publishingpublish:writeget_publish_options · publish_story · unpublish_storyInspect get_publish_options, review the video and audience, then publish.
Social postingsocial:publishlist_social_accounts · social_publish · get_social_postNeeds account and caption approval; poll get_social_post for the result.
Generate an overlay asset ★stories:generategenerate_layer_assetQuote generate_layer_asset first; inspect its result with get_layer_asset before using the asset on the timeline.
Generate an overlay assetstories:readget_layer_assetQuote generate_layer_asset first; inspect its result with get_layer_asset before using the asset on the timeline.
Creator hiring, jobs and messageshires:readsearch_hire_listings · quote_hire · search_video_job_creators · list_hires · get_hire · get_video_job · list_casting_calls · list_call_applicants · list_application_characters · list_my_applications · list_clip_calls · get_clip_call · get_my_hire_offers · get_clip_service · list_threads · read_threadExternal money and commitments: review quote_hire before send_hire_request. pay_hire returns Stripe checkout for the user to pay. A video job (hire_creator_clips, post_clip_call) is always the whole episode; clipIds from older calls is ignored. open_clip_workspace and send_clip_take are legacy. Review deliveries before accepting payouts. A first character listing needs explicit rights and any real-person likeness authorization.
Creator hiring, jobs and messageshires:writesend_hire_request · hire_creator_clips · pay_hire · attach_hired_character · respond_hire · deliver_clip · open_clip_workspace · send_clip_take · review_delivery · hire_post_action · cancel_hire_work · send_hire_message · submit_outline · review_outline · send_job_script · send_job_video · ask_job_budget · add_job_budget · create_casting_call · update_casting_call · close_casting_call · apply_to_casting_call · update_application · post_clip_call · offer_on_clip_call · withdraw_clip_offer · pick_clip_offer · close_clip_call · set_hire_offer · set_character_listing · set_clip_service · mark_thread_read · start_thread · send_messageExternal money and commitments: review quote_hire before send_hire_request. pay_hire returns Stripe checkout for the user to pay. A video job (hire_creator_clips, post_clip_call) is always the whole episode; clipIds from older calls is ignored. open_clip_workspace and send_clip_take are legacy. Review deliveries before accepting payouts. A first character listing needs explicit rights and any real-person likeness authorization.
Yoh Ads shows and campaignsads:readlist_brand_shows · get_brand_show · get_show_episode · get_show_results · get_show_activity · get_show_plan · get_series_ads · list_series_ad_pages · list_meta_ad_accountsCreate and review a show, then accept it. Paid writing, video and autopilot also need ads:write. get_show_plan previews autopilot; inspect posting and Meta budgets as well as credits. A zero-credit confirmation can still authorize external ad spending.
Yoh Ads shows and campaignsads:writecreate_brand_show · reread_brand_show · accept_brand_show · update_brand_show · update_show_action · create_show_episode_version · set_series_ad_status · set_up_series_ads · add_episode_to_adsCreate and review a show, then accept it. Paid writing, video and autopilot also need ads:write. get_show_plan previews autopilot; inspect posting and Meta budgets as well as credits. A zero-credit confirmation can still authorize external ad spending.
Yoh Ads shows and campaigns ★stories:generatewrite_show_episode · make_show_episode · run_show_autopilotCreate and review a show, then accept it. Paid writing, video and autopilot also need ads:write. get_show_plan previews autopilot; inspect posting and Meta budgets as well as credits. A zero-credit confirmation can still authorize external ad spending.
Script helpers, account and activitystories:readget_script_marks · list_assistant_history · get_earnings · get_credit_history · get_story_spend · list_notifications · list_api_keys · get_profile · list_episode_commentsRead script marks before targeted text edits. Inspect earnings, credit history, notifications and profile. API key listings do not reveal secrets; revoking the current key disconnects the client.
Script helpers, account and activitystories:writekeep_clip_as_is · write_still_prompt · write_time_beats · suggest_clip_duration · mark_notifications_read · revoke_api_key · update_profile · remove_episode_commentRead script marks before targeted text edits. Inspect earnings, credit history, notifications and profile. API key listings do not reveal secrets; revoking the current key disconnects the client.
Script helpers, account and activityassets:readsearch_stock_mediaRead script marks before targeted text edits. Inspect earnings, credit history, notifications and profile. API key listings do not reveal secrets; revoking the current key disconnects the client.

Pengeluaran dijaga dua lapis Alat ★ mengembalikan penawaran kredit dan confirmToken bertanda tangan sebelum membelanjakan kredit. Tampilkan penawaran dan lanjutkan hanya setelah persetujuan tegas; alat ini memerlukan stories:generate. Ini tidak menjamin alat lain gratis: ask_assistant langsung menagih giliran teks dan memerlukan stories:generate (kunci yang hanya memiliki stories:write tidak dapat menjalankannya). Pembuatan terjadwal, pembayaran kreator, dan iklan Meta memiliki aturan persetujuan dan biaya tersendiri.

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 — sebagian besar alat mengembalikan runId yang di-poll agen dengan get_run; beberapa (create_story, render_story, cast art) mengembalikan jobId untuk 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://www.yoh.app/api/mcp --key sk_live_YOUR_API_KEY
# read-only variant:
node scripts/mcp-smoke.mjs --url https://www.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. /settings/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

HTTP 409 with error.code = generation_budget_exceeded means your optional X-Max-Generation-Credits cap was exceeded. The response includes quote.credits; nothing was charged. Change the request or explicitly raise your budget to retry. A malformed cap returns 400. Other 409 conflicts can mean an idempotency key was reused with different input or work is already running. Template-version generation uses its own persisted quoteId contract; see its OpenAPI reference.

Story create (POST /api/v1/stories/generate and /estimate, MCP create_story / estimate_story) checks the series before pricing: 404 series_not_found when the key's user cannot open that seriesId, 403 series_forbidden when they can open it but may not add episodes to it. Nothing is charged.

Rate limits are per key: 5 generation calls, 60 reads and 60 writes a minute. Each call counts once: an MCP tool call is one call even when it runs several v1 steps, and POST /stories/:id/swap-asset is one generation call for every clip it switches.

Sebagian besar endpoint mengembalikan objek error dengan code yang dapat dibaca mesin. Endpoint khusus dapat mengembalikan pesan error berupa string atau kode tingkat atas; selalu periksa status HTTP.

{ "error": { "code": "insufficient_credits", "message": "...", "status": 402, "required": 120, "available": 30, "balance": 30, "topUpUrl": "https://yoh.app/studio/episodes/…?credits_sheet=1&credits_need=120" } }
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.