เอกสารอ้างอิง API
สร้างวิดีโอ AI ผ่านโปรแกรม ส่งพรอมป์ ตรวจสอบสถานะ แล้วรับ URL วิดีโอโดยตรง — ทั้งหมดผ่าน REST
Base URL: https://www.yoh.appตัวอย่างโค้ดจะใช้ตัวยึดตำแหน่ง sk_live_YOUR_API_KEY จนกว่าคุณจะลงชื่อเข้าใช้
เริ่มต้นใช้งาน
สร้างวิดีโอแรกของคุณด้วยการเรียก API เพียง 3 ครั้ง: สร้าง → ตรวจสอบสถานะ → ดาวน์โหลด
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.
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.
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.
{
"jobId": "cmnk8wu2q0001q3vpmqv93nmy",
"status": "queued",
"pollUrl": "/api/v1/stories/jobs/cmnk8wu2q0001q3vpmqv93nmy"
}curl https://www.yoh.app/api/v1/stories/jobs/cmnk8wu2q0001q3vpmqv93nmy \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"{
"status": "completed", "progress": 100,
"storyId": "cmnk8xxnx000004lbd7siief7",
"videoUrl": "https://cdn.example.com/video/clip.mp4",
"error": null
}เท่านี้ก็เรียบร้อย videoUrl คือ URL ที่เข้าถึงได้แบบสาธารณะ คุณสามารถดาวน์โหลด สตรีม หรือแชร์ได้ทันที
การยืนยันตัวตน
คำขอ API ทั้งหมดต้องแนบ API key ของคุณในส่วนหัว Authorization โดยใช้รูปแบบ Bearer
Authorization: Bearer sk_live_YOUR_API_KEYAPI key จะขึ้นต้นด้วย sk_live_ คุณสามารถสร้างและจัดการ key ได้จากAPI Keys page /settings/api-keys
เก็บ API key ของคุณเป็นความลับ อย่าเปิดเผยไว้ในโค้ดฝั่งไคลเอนต์หรือ repository สาธารณะ หาก key ถูกเปิดเผย ให้เพิกถอนทันทีจากหน้า API Keys
ขอบเขตสิทธิ์ & เครดิต
API key แต่ละอันจะได้รับสิทธิ์หนึ่งขอบเขตขึ้นไปที่ควบคุมว่า endpoint ใดที่เข้าถึงได้ การสร้างข้อความเรื่องราวมีค่าใช้จ่าย 55 เครดิตต่องาน ส่วนขอบเขตที่สร้างวิดีโอคลิปหรือการเรนเดอร์สุดท้ายด้วย (รวมถึงขอบเขตเริ่มต้นคือ full) จะคิดค่าใช้จ่ายล่วงหน้าเพิ่มเติม เครดิตจะถูกคืนหากงานล้มเหลวหลังจากลองใหม่ครบทุกครั้งแล้ว
| ขอบเขตสิทธิ์ | ให้สิทธิ์เข้าถึง |
|---|---|
| stories:generate | POST /api/v1/stories/generate, POST /stories/:id/assets, POST /stories/:id/storyboard, POST /stories/:id/videos, POST /stories/:id/render, POST /stories/:id/clips/:clipId/image, POST /stories/:id/clips/:clipId/video, POST /stories/:id/clips/:clipId/video-edit, POST /stories/:id/characters/:assetId/sheet, POST /stories/:id/soundtrack, POST /stories/:id/assets/:assetId/image, POST /stories/:id/clips/:clipId/regenerate, POST /stories/:id/continue, POST /stories/:id/clips-all, POST /stories/:id/clips/:clipId/grid-reference — and the story-asset edits: PATCH /stories/:id/characters|scenes|items/:assetId. Also implies assets:write and series:write, so keys issued before those scopes existed keep working. |
| stories:read | GET /api/v1/stories, GET /api/v1/stories/:id, GET /api/v1/stories/jobs/:id, GET /api/v1/credits, 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:write | The 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:read | GET .../characters, .../scenes, .../items, .../clips, GET .../characters/:assetId/hire-looks, GET /api/v1/series/:id/assets |
| assets:write | POST /api/v1/series/:id/assets, PATCH/DELETE /api/v1/series/:id/assets/:assetId, POST /api/v1/uploads, POST /api/v1/uploads/presign (a one-time PUT URL for an audio, video or image file) |
| series:read | GET /api/v1/series, GET /api/v1/series/:id |
| series:write | POST /api/v1/series, PATCH /api/v1/series/:id |
| social:publish | POST /api/v1/social/publish, GET /api/v1/social/publish/:postId |
| accounts:manage | GET/POST /api/v1/accounts, GET/POST /api/v1/accounts/:accountId/credits. For platforms that serve many customers: provision an account per customer and fund it from your own balance. Implied by nothing — an existing key never gains it. |
| hires:read | The 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:write | The 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:read | The MCP Yoh Ads reads: shows, briefs and results. Implied by nothing. |
| ads:write | The 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:write | The MCP publish tools: publish and unpublish an episode on Yoh (not social posting, which is social:publish). Implied by nothing. |
สร้างวิดีโอ
เพิ่มงานสร้างวิดีโอเข้าคิว
/api/v1/stories/generatescope: stories:generateส่งพรอมป์และคืนค่า jobId ทันที วิดีโอจะถูกสร้างแบบอะซิงโครนัส ตรวจสอบสถานะได้ที่ GET /api/v1/stories/jobs/:jobId
เนื้อหาคำขอ
| ฟิลด์ | ประเภท | สถานะ | คำอธิบาย |
|---|---|---|---|
prompt | string | จำเป็น | คำอธิบายเรื่องราว สูงสุด 20000 ตัวอักษร |
aspectRatio | string | ไม่บังคับ | หนึ่งใน 16:9, 9:16, 1:1, 21:9 ค่าเริ่มต้น: 16:9 |
renderVideo | boolean | ไม่บังคับ | false — คืนค่าคลิป AI แรก (เร็ว ~10 นาที) true — เรนเดอร์คลิปทั้งหมดรวมเป็น MP4 เดียวผ่าน Remotion (~12–15 นาที) |
scope | string | ไม่บังคับ | story (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. |
title | string | ไม่บังคับ | ชื่อที่แสดงของเรื่องราว |
contentType | string | ไม่บังคับ | หมวดหมู่รูปแบบเรื่องราว หนึ่งใน cinematic_story, news_analysis, persona_channel, ad_spot ค่าเริ่มต้น: cinematic_story |
locale | string | ไม่บังคับ | รหัสภาษา เช่น en, zh-TW ค่าเริ่มต้น: en |
seriesId | string | ไม่บังคับ | แนบเข้ากับซีรีส์ที่มีอยู่แล้วเพื่อให้ภาพตัวละครมีความสอดคล้องกัน |
assets | array | ไม่บังคับ | The cast and props for this story — reference assets already in your series library, or attach one-off assets inline. See the table below. |
continuityMode | string | ไม่บังคับ | With a seriesId: how much of the previous episodes this one carries. episodic_continuous (default) picks up open threads, finale resolves them, one_off_episode keeps series plot out entirely. |
isFinale | boolean | ไม่บังคับ | Implied by continuityMode: "finale"; you rarely need both. |
allowMultiStory | boolean | ไม่บังคับ | Let one request split into several episodes when the prompt clearly carries more than one. Default false — N episodes means N charges, so it is opt-in. |
seriesContinuityContext | object | ไม่บังคับ | Escape hatch. Supplying it replaces the continuity we assemble from your series (open threads plus the last three episode recaps), so an incomplete object produces a weaker episode than sending nothing. Prefer continuityMode. |
assets[] ไม่บังคับ
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.
| ฟิลด์ | ประเภท | คำอธิบาย |
|---|---|---|
seriesAssetId | string | Reference an existing library asset. Everything else is optional when this is set. |
type | string | character · scene · item · reference. Required for an inline asset. |
name | string | Required for an inline asset. Also how the writer refers to it — name it in your prompt for it to appear. |
description | string | Overrides the library description for this story only. |
imageUrl | string | Image for an inline asset. Re-hosted on our storage. |
itemType | string | Items only: prop · product · logo. A logo is composited flat, never given 3D depth. |
isHeroProduct | boolean | ad_spot: the advertised product — appears in every clip. Fills heroItemId for you. |
isPrimaryBackground | boolean | news_analysis / persona_channel: the set reused across every shot. ad_spot: the real place (e.g. your shop) used in the shots that name it. Fills primaryBackgroundAssetId for you. |
useAsReferenceFrame | boolean | Use this image as the first frame / reference for image-to-video on clips featuring the asset. |
gender | string | Characters only: surfaced to the writer. |
age | string | Characters only: surfaced to the writer. |
personalityTraits | string[] | Characters only: surfaced to the writer. |
timeOfDay | string | Scenes only, e.g. dusk. |
weather | string | Scenes only, e.g. rain. |
persist | boolean | Accrue an inline asset into the series library so later stories can reference it by id. Default false. |
Characters, scenes and items only enter the story when the prompt names them — attaching a whole series library does not drag its entire cast into every episode. The two pin flags are the exception: a pinned hero product or primary background is guaranteed regardless of whether the script says its name.
globalConfig ไม่บังคับ
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.
| ฟิลด์ | ประเภท | ค่า | ค่าเริ่มต้น |
|---|---|---|---|
storyInputMode | string | auto · 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 |
durationSec | number | 15–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) |
clipCount | number | 2–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") | — |
aspectRatio | string | 16:9 · 9:16 · 1:1 · 21:9 | 1:1 (9:16 for ad_spot, persona_channel, news_analysis; a series' episode shape when seriesId is given) |
resolution | string | 480p · 720p · 1080p | 720p |
pace | string | slow · normal · fast | normal |
structure | string | auto · 3_act · 4_act · montage | auto |
captionStyle | string | standard · 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 style | words for 9:16, else standard |
qualityTier | string | draft · balanced · premium (premium selects the advanced model lane) | balanced |
stylePreset | string | auto, or a visual style id (list_style_presets kind visual). An unknown id is refused 400; custom style words go in style.cinematicStyle | auto |
platformTarget | string | ADVISORY — shown to the writer as context; nothing enforces it. youtube · tiktok · instagram · x | youtube |
safetyMode | string | ADVISORY — shown to the writer as context; not a content filter. standard · strict | standard |
seed | string | any string — shared across clips for continuity (API-only) | — |
videoModel | string | a model id, or auto | auto |
videoEditModel | string | video editing only — 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 |
imageModel | string | clip images (the optional storyboard) + scene/item images — a model id, or auto (GPT Image 2.5) | auto |
characterImageModel | string | character 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 |
textFamily | string | the 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_story | auto |
modelStrategy | string | auto · fixed · cost_optimized · quality_first · hero_premium | auto |
generationMode | string | DEPRECATED — ignored. The selected model determines frame or reference inputs. | auto |
locale | string | en · zh-TW · ja · ko · th · id (also accepted top-level) | en |
contentTypeConfig ไม่บังคับ
Optional per-contentType creative controls. Fields are validated against the selected content type; omitted fields fall back to sensible defaults. Pass the object matching your contentType. The two pin ids below are usually easier to set as isHeroProduct / isPrimaryBackground on an assets[] entry — an id you write here wins if you send both.
| contentType | ฟิลด์ |
|---|---|
| cinematic_story | (no per-type fields — format craft is built in) |
| news_analysis | primaryBackgroundAssetId, primaryBackgroundName (pin a studio scene across anchor shots) |
| persona_channel | primaryBackgroundAssetId, primaryBackgroundName (pin the persona's set) |
| ad_spot | heroItemId, heroProductName, heroTagline, heroKeyBenefit, 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. |
ฟิลด์ที่ไม่บังคับอื่นๆ
| ฟิลด์ | ประเภท | คำอธิบาย |
|---|---|---|
storyQualityConfig | object | { 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). |
castArt | boolean | scope "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. |
style | object | Style-consistency overrides: cinematicStyle, era, colorPalette, lightingStyle, filmGrain, lensCharacter, materialEmphasis, styleCategory, negativeHints, stylePreset. |
seriesContinuityContext | object | Series continuity context for episodic generation. Used together with seriesId. |
Fields that aren't part of the selected contentType's schema are silently ignored — cinematic_story takes no per-type fields at all, so pass contentTypeConfig only for ad_spot, news_analysis, or persona_channel.
curl -X POST 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"
}
}'การตอบกลับ 202 Accepted
{ "jobId": "cmnk8wu2q0001q3vpmqv93nmy", "status": "queued", "pollUrl": "/api/v1/stories/jobs/..." }เอนด์พอยต์ทีละขั้นตอน
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.
/api/v1/stories/:storyId/assetsGenerate 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.
/api/v1/stories/:storyId/storyboardOptional 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.
/api/v1/stories/:storyId/videosGenerate 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).
/api/v1/stories/:storyId/renderStitch 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.
ทีละคลิป
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).
/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.
/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.
estimatedCredits) | Scope: stories:generate/api/v1/stories/:storyId/clips/:clipId/regenerateRebuild 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.
breakdown | Scope: stories:generate/api/v1/stories/:storyId/clips/:clipId/grid-referenceClip authoring supports description for scene and image context, and optional motionDescription for video movement and timed beats. Video generation uses both. When motion is empty, video generation automatically creates and saves timed motion first. Send null to prepare new motion on the next video run. Draw a multi-panel continuity reference sheet for one clip. The panels are derived from the clip's time beats — an authored beat track when motionDescription carries one, otherwise the beat plan its generation length implies — so the sheet mirrors that shot rather than a fixed set of generic moments. Two panels per beat, snapped to a supported grid layout. 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).
/api/v1/stories/:storyId/assets/:assetId/image/api/v1/stories/:storyId/characters/:assetId/sheetDraw (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.
/api/v1/stories/:storyId/characters/:assetId/hire-looks/api/v1/stories/:storyId/characters/:assetId/hire-looksThe 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.
/api/v1/stories/:storyId/clips-allGenerate 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.
breakdown | Scope: stories:generate/api/v1/stories/:storyId/continueWrite a NEW storyline branching off an existing clip — an alternative continuation the story keeps alongside the original, which is untouched. Required body: branchFromClipId. Optional prompt (where the story goes) and clipCount (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.
/api/v1/stories/preview-scriptReview 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.
/api/v1/stories/:storyId/readinessWhat 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.
/api/v1/stories/:storyId/attentionEverything 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).
/api/v1/stories/:storyId/spendCredits 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.
/api/v1/stories/:storyId/clip-checkCheck 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.
/api/v1/stories/:storyId/clip-checkThe 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.
/api/v1/stories/:storyId/clip-check/applyApply 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.
/api/v1/stories/:storyId/voice-only-speakersA 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.
/api/v1/stories/:storyId/rendersThe story's finished videos, newest first, with latestSuccessful called out so a failed render is never mistaken for the current cut. A story keeps every render. Optional ?limit (max 50). 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).
/api/v1/stories/:storyId/clips/:clipId/video-editRewrite 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.
estimatedCredits is the accepted price | Scope: stories:generate/api/v1/stories/:storyId/clips/:clipId/versionGET 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.
/api/v1/stories/:storyId/swap-assetPut 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.
/api/v1/stories/:storyId/versionsMake 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.
/api/v1/stories/:storyId/versionsEvery 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.
/api/v1/stories/:storyId/versionGET 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.
# 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แก้ไขเรื่อง
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.
/api/v1/stories/:storyId/clips/:clipIdEdit 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).
/api/v1/stories/:storyId/clipsInsert 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.
/api/v1/stories/:storyId/clips/:clipId/duplicateThe 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.
/api/v1/stories/:storyId/clips/:clipId/audio-syncA 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.
/api/v1/stories/:storyId/separate-audioSeparate 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.
/api/v1/stories/:storyId/clipsReorder. 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.
/api/v1/stories/:storyId/clips/:clipIdCut 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).
# 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"# 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ตรวจสอบสถานะงาน
ดูสถานะงาน
/api/v1/stories/jobs/:jobIdscope: stories:readตรวจสอบ endpoint นี้หลังจากเรียก POST /api/v1/stories/generate โดยทั่วไปงานจะใช้เวลา 8–15 นาที ขึ้นอยู่กับความยาวของเรื่องราวและว่าเปิดใช้ renderVideo หรือไม่ แนะนำให้ตรวจสอบทุก 15–30 วินาที
วงจรสถานะของงาน
queuedgenerating_storygenerating_assetsgenerating_videosrendering_videocompletedหรือfailedWhich 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).
ฟิลด์ในการตอบกลับ
| ฟิลด์ | ประเภท | คำอธิบาย |
|---|---|---|
jobId | string | รหัสเฉพาะของงาน |
status | string | สถานะปัจจุบัน ดูวงจรสถานะด้านบน |
progress | number | จำนวนเต็ม 0–100 แสดงเปอร์เซ็นต์ความคืบหน้า |
storyId | string|null | จะถูกตั้งค่าเมื่อการสร้างเรื่องราวเสร็จสมบูรณ์ ใช้เพื่อดึงข้อมูลอาเซ็ท |
renderVideo | boolean | ระบุว่ามีการร้องขอ MP4 สุดท้ายแบบรวมคลิปหรือไม่ |
videoUrl | string|null | URL วิดีโอแบบสมบูรณ์เมื่อสถานะเป็น completed อาจเป็น MP4 แบบรวมคลิป (renderVideo: true) หรือวิดีโอ AI ของคลิปแรก (renderVideo: false) |
error | string|null | ข้อความแสดงข้อผิดพลาดที่อ่านเข้าใจง่าย จะถูกตั้งค่าเมื่อเกิดความล้มเหลว — และขณะกำลังลองงานใหม่ด้วย ซึ่งสถานะจะกลับไปเป็น queued เป็น null เสมอเมื่อสำเร็จ |
waitingFor / message | string (optional) | มีค่าเมื่องานรอผลจากผู้ให้บริการ (status ยังเป็น queued และ error เป็น null) ค่าคือ "provider" และ message บอกว่ากำลังรออะไร ให้ polling ต่อไป |
execution | object (optional) | Workflow run ที่รันงานนี้: { version, runId, phase, heartbeatAt, status } ติดตามได้ที่ GET /api/runs/:runId (หรือ /stream) งานเก่าจะไม่มีฟิลด์นี้ |
createdAt / startedAt / completedAt | string (ISO) | ประทับเวลา |
runId / runUrl | string (202 only) | อยู่ในคำตอบ 202 ทุกครั้งที่เริ่มงาน (ไม่ใช่ในการ polling นี้): run ที่เริ่มให้งานนั้นและ URL สถานะ จะใช้หรือไม่ก็ได้ polling endpoint นี้ก็เพียงพอ |
เรื่องราว
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.
รายการเรื่องราว
/api/v1/storiesscope: stories:readคืนค่ารายการเรื่องราวของคุณแบบแบ่งหน้า
พารามิเตอร์การค้นหา
| พารามิเตอร์ | ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|
page | 1 | หมายเลขหน้า (≥ 1) |
limit | 20 | ผลลัพธ์ต่อหน้า (1–100) Capped at 100. |
status | — | กรองตามสถานะของเรื่องราว |
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 }
}ดูเรื่องราวหนึ่งรายการ
/api/v1/stories/:storyIdscope: stories:readคืนค่าข้อมูลเมตาแบบสมบูรณ์สำหรับเรื่องราวหนึ่งรายการ รวมถึงจำนวนตัวละคร ฉาก ไอเทม และคลิป
{
"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"
}ภาพรวมอาเซ็ท
หลังจากงานเสร็จสมบูรณ์ คุณสามารถดึงอาเซ็ททั้งหมดที่สร้างสำหรับเรื่องราวได้โดยใช้ storyId ที่ได้จากงานนั้น endpoint อาเซ็ททั้งหมดต้องใช้ขอบเขตสิทธิ์ assets:read
GET /api/v1/stories/:id/charactersตัวละคร — ชื่อ คำอธิบาย บทบาท imageUrl ข้อมูลเสียง
GET /api/v1/stories/:id/scenesฉาก — คำอธิบาย ช่วงเวลาของวัน สภาพอากาศ แสง imageUrl
GET /api/v1/stories/:id/itemsอุปกรณ์ประกอบฉาก / ไอเทม — คำอธิบาย ความสำคัญ imageUrl
GET /api/v1/stories/:id/clipsคลิป — บรรยาย ความยาว videoUrl imageUrl audioUrl
ตัวละคร
รายการตัวละคร
/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
}]
}คลิป & วิดีโอ
รายการคลิป
/api/v1/stories/:storyId/clipsscope: assets:readแต่ละคลิปคือส่วนของฉากที่มีวิดีโอสร้างโดย AI ภาพนิ่ง และเสียงเป็นของตัวเอง ฟิลด์ URL ทั้งหมดเป็น URL แบบสมบูรณ์และเข้าถึงได้แบบสาธารณะ
{
"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"
}]
}เมื่อ renderVideo เป็น false videoUrl ของงานจะชี้ไปยังวิดีโอของคลิปแรก ใช้ endpoint นี้เพื่อรับ URL วิดีโอของคลิปแต่ละรายการ
คลังอาเซ็ทของซีรีส์
อัปโหลดครั้งเดียว ใช้ซ้ำได้ตลอด
/api/v1/series/:seriesId/assetsscope: assets:writeเป็นคลังเดียวกับที่แผงอาเซ็ทในสตูดิโอเขียนลงไป อัปโหลดตัวละคร ฉากหลัง พร็อพ หรือภาพอ้างอิงสไตล์ แล้วเก็บ id ที่ได้กลับมา จากนั้นตอนสร้างงานให้ส่ง id นั้นเป็น seriesAssetId ในอาร์เรย์ assets โดยไม่ต้องสร้างลิงก์รูปใหม่ทุกครั้ง รูปที่คุณส่งมาจะถูกนำไปโฮสต์บนพื้นที่จัดเก็บของเรา ดังนั้น id จะยังใช้งานได้แม้ URL ต้นทางของคุณจะหมดอายุ
| ฟิลด์ | ประเภท | คำอธิบาย |
|---|---|---|
type | string | character, scene, item หรือ reference |
name | string | ชื่อที่แสดง และเป็นคีย์ที่ใช้จับคู่ในการสร้างงานครั้งถัดไป |
imageUrl | string | URL สาธารณะของรูปภาพ หรือจะส่ง imageBase64 แทนก็ได้ |
itemType | string | เฉพาะพร็อพ: prop, product หรือ logo โดย logo จะถูกวางแบบแบน ไม่ใส่มิติ 3 มิติ |
isHeroProduct | boolean | เฉพาะพร็อพ: สินค้าหลักของโฆษณานี้ จะปรากฏในทุกคลิป |
isPrimaryBackground | boolean | เฉพาะฉาก: สตูดิโอ/ฉากที่ใช้ซ้ำในทุกช็อตของผู้ประกาศ |
seriesDuplicate | string | เฉพาะตัวละคร หลังถามผู้ใช้แล้ว: "new" เพิ่มตัวละครตัวที่สองที่ใช้ชื่อซึ่งซีรีส์มีอยู่แล้ว |
- ส่งอาเซ็ทเดียว หรือใช้อาร์เรย์ "assets" เพื่อบันทึกพร้อมกันได้สูงสุด 100 รายการ
- โดยค่าเริ่มต้น แอสเซ็ตที่ชื่อตรงกันทุกตัวอักษรจะถูกรวมกัน การอัปโหลดแคตตาล็อกเดิมซ้ำจึงไม่ซ้ำซ้อน ยกเว้นตัวละครที่ซีรีส์มีชื่อนั้นอยู่แล้ว ซึ่งจะได้คำตอบ 409 SERIES_NAME_EXISTS พร้อมตัวละครเดิม และไม่บันทึกอะไร ถามผู้ใช้ก่อน แล้วส่ง seriesDuplicate "new" เพื่อเพิ่มตัวที่สอง หรือ ?mergeStrategy=exact เพื่ออัปเดตตัวเดิม ?mergeStrategy=fuzzy ยืดหยุ่นกับการเปลี่ยนชื่อ ส่วน insert สร้างแอสเซ็ตใหม่เสมอ
- DELETE จะตอบ 409 SERIES_ASSET_IN_USE พร้อมตอนที่ยังใช้แอสเซ็ตนั้น (stories: id, title, clipCount) และไม่ลบอะไร ถามผู้ใช้ก่อน แล้วส่ง confirm: true (ใน body หรือ ?confirm=true) ตอนต่าง ๆ ยังเก็บสำเนาของตัวเองและคลิปไม่เปลี่ยน ตัวละครที่อยู่ในการจ้างที่ยังเปิดอยู่จะได้ 409 SERIES_ASSET_HIRED (hires) และลบไม่ได้จนกว่าการจ้างจะจบ
- ตั้ง isHeroProduct บนพร็อพ (ad_spot) หรือ isPrimaryBackground บนฉาก (news_analysis / persona_channel) เพื่อปักหมุด ระบบจะแปลงค่าให้อัตโนมัติตอนสร้างงาน คุณไม่ต้องเขียน heroItemId เอง
| ขอบเขตสิทธิ์ | ให้สิทธิ์เข้าถึง |
|---|---|
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.
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:
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", ... } }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.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.
อาเซ็ทของซีรีส์
ลงทะเบียนภาพตัวละครสำหรับซีรีส์
/api/v1/series-assets/ensure-character-imagescope: assets:writePOST /api/v1/series-assets/ensure-character-image ยังใช้งานได้และไม่มีการเปลี่ยนแปลง แต่รองรับเฉพาะตัวละคร แนะนำให้ใช้เอนด์พอยต์นี้แทน
อัปโหลดและบันทึกภาพอ้างอิงตัวละครสำหรับซีรีส์ เพื่อให้ AI ใช้ใบหน้า/ลุคที่สอดคล้องกันในทุกเรื่องราวของซีรีส์นั้น การดำเนินการนี้เป็นแบบ idempotent — เรียกซ้ำหลายครั้งด้วยชื่อตัวละครเดียวกันได้อย่างปลอดภัย
| ฟิลด์ | ประเภท | คำอธิบาย |
|---|---|---|
seriesId | string | รหัสของซีรีส์ที่จะแนบตัวละครเข้าไป |
characterName | string | ชื่อของตัวละคร (ใช้เป็นคีย์เฉพาะภายในซีรีส์) |
imageUrl | string | URL ที่เข้าถึงได้แบบสาธารณะของภาพอ้างอิงตัวละคร |
บัญชีที่จัดการให้
สำหรับแพลตฟอร์มที่ให้บริการลูกค้าจำนวนมาก แทนที่จะใช้คีย์เดียวและกองเครดิตเดียวร่วมกันทั้งหมด — ซึ่งลูกค้ารายที่ใช้หนักจะดูดยอดคงเหลือของคนอื่นจนหมด และไม่มีใครบอกได้ว่าใครใช้ไปเท่าไร — ให้เปิดบัญชีแยกต่อลูกค้าหนึ่งราย แล้วเติมเครดิตให้แต่ละบัญชีจากยอดคงเหลือของคุณเอง ลูกค้าของคุณไม่ต้องสมัครที่นี่และไม่ต้องจ่ายเงินให้เรา ความสัมพันธ์ทางการค้ายังเป็นของคุณ
เปิดบัญชี
/api/v1/accountsscope: accounts:manageexternalId คือรหัสของคุณเองที่ใช้แทนลูกค้ารายนั้น การเรียกซ้ำด้วยค่าเดิมจะคืนบัญชีเดิมแทนที่จะสร้างบัญชีที่สอง การลองใหม่จึงปลอดภัย API key จะถูกส่งกลับเพียงครั้งเดียว ตอนสร้างครั้งแรก — เราเก็บไว้เป็นแฮชและไม่สามารถแสดงให้ดูอีกได้ จึงควรบันทึกไว้ทันทีที่ได้รับ บัญชีใหม่จะเริ่มต้นที่ยอดคงเหลือศูนย์
| ฟิลด์ | ประเภท | คำอธิบาย |
|---|---|---|
externalId | string | จำเป็น รหัสของคุณสำหรับลูกค้ารายนี้ เช่น tenant id ต้องไม่ซ้ำภายในแพลตฟอร์มเดียวกัน |
name | string | ป้ายกำกับ (ไม่บังคับ) สำหรับใช้ทำรายงานของคุณเอง |
เปลี่ยนคีย์ของบัญชีที่จัดการ
/api/v1/accounts/:accountId/api-keysscope: accounts:manageหากคีย์เริ่มต้นสูญหาย ให้เรียกจุดเชื่อมต่อนี้ด้วยคีย์ของแพลตฟอร์มเจ้าของโดยไม่ต้องส่งเนื้อหาคำขอ ใช้รหัสบัญชีหรือ externalId ได้ ระบบจะเพิกถอนคีย์เดิมทั้งหมดและส่ง apiKey ใหม่เพียงครั้งเดียว (201) ภายในธุรกรรมเดียว โปรดเก็บคีย์อย่างปลอดภัย การเรียกซ้ำจะเปลี่ยนคีย์อีกครั้ง ส่วนการลองสร้างบัญชีซ้ำตามปกติจะไม่เปลี่ยนคีย์ที่มีอยู่
เติมเครดิตให้บัญชี
/api/v1/accounts/:accountId/creditsscope: accounts:manageย้ายเครดิตจากยอดคงเหลือ "ของคุณ" ไปยังบัญชีนั้น — เป็นการโอน ไม่ใช่การให้เปล่า คุณจึงแจกได้เท่าที่ซื้อไว้จริงเท่านั้น จะคืน 402 เมื่อยอดคงเหลือของคุณไม่พอ ส่วน :accountId รับได้ทั้งรหัสบัญชีของเราและ externalId ของคุณ
| ฟิลด์ | ประเภท | คำอธิบาย |
|---|---|---|
amount | integer | จำเป็น จำนวนเครดิตที่จะย้าย ต้องเป็นจำนวนบวก |
idempotencyKey | string | ใส่รหัสใบแจ้งหนี้หรือรอบการสมัครสมาชิกของคุณ การเรียกซ้ำหรือส่งซ้ำจะคืนรายการโอนเดิมแทนการเติมเครดิตซ้ำสองครั้ง ลองใหม่ด้วยคีย์ บัญชีผู้รับ และจำนวนเดิม การลองซ้ำจะไม่โอนเครดิตอีก แม้ยอดคงเหลือของแพลตฟอร์มจะหมดแล้ว หากเปลี่ยนผู้รับหรือจำนวนจะได้รับ 409 คีย์ที่ยาวเกินไปจะถูกปฏิเสธแทนการตัดให้สั้นลง |
description | string | หมายเหตุ (ไม่บังคับ) แสดงทั้งสองฝั่งของการโอนในประวัติเครดิต |
GET /api/v1/accounts จะแสดงรายการบัญชีของคุณพร้อมยอดคงเหลือและยอดใช้จ่ายสะสมของแต่ละบัญชี ส่วน GET /api/v1/accounts/:accountId/credits ใช้อ่านทีละบัญชี คีย์ของบัญชีที่จัดการให้จะมีเฉพาะขอบเขตการสร้างเท่านั้น — ไม่สามารถเปิดหรือเติมเครดิตให้บัญชีใดได้เลย
Claude Code และ Codex (MCP)
Yoh มีเซิร์ฟเวอร์ MCP ให้ เพื่อให้เอเจนต์ AI (Claude Code, Codex, Claude Desktop หรือไคลเอนต์ MCP ใดก็ได้) สร้างและแก้ไขสตอรี่ในบัญชีของคุณได้ ทั้งเขียนสตอรี่ใหม่จากไอเดีย คัดตัวละครและสร้างวิดีโอ หรือแก้เนื้อหาคลิปและบทพูด การเลื่อนและตัดบนไทม์ไลน์ และเลเยอร์อิสระ ทุกอย่างที่ใช้เครดิตจะรอให้คุณอนุมัติอย่างชัดเจนทุกครั้ง มันทำงานตามกฎเดียวกับตัวแก้ไขในเบราว์เซอร์ การแก้ไขจากเทอร์มินัลจึงลงตำแหน่งเดียวกับที่การลากจะได้
ใช้งานได้ 3 แบบ
เลือกแบบที่เข้ากับเครื่องมือของเอเจนต์ สกิล yoh ด้านล่างสอนครบทั้งสามแบบ
- MCP + เบราว์เซอร์ (แนะนำใน Codex, Claude Code และแอป Claude) เอเจนต์ทำงานผ่านเครื่องมือ MCP นี้ และควบคุมแท็บเบราว์เซอร์ไปพร้อมกัน (Claude in Chrome, เบราว์เซอร์ของ Codex): หลังแต่ละขั้น เอเจนต์จะเปิดลิงก์ที่มากับคำตอบ (ตารางสคริปต์หลังสร้างเรื่องใหม่, แผงคลิปหลังทำวิดีโอคลิป, รายการ Generate All ระหว่างที่กำลังทำ, ตัวแก้ไขวิดีโอสำหรับไทม์ไลน์และเลเยอร์ข้อความ, ไฟล์ MP4 หลังเรนเดอร์) แล้วตรวจผลบนหน้าจริง ลงชื่อเข้าใช้ในเบราว์เซอร์นั้นครั้งเดียวด้วยบัญชีเดียวกับคีย์ API MCP และหน้าเว็บใช้ข้อมูลชุดเดียวกัน หน้าตอนจะอัปเดตเองภายในไม่กี่วินาทีหลังการเปลี่ยนแปลงผ่าน MCP และเอเจนต์จะรีโหลดตัวแก้ไขวิดีโอหลังแก้ไทม์ไลน์ การใช้เครดิตแต่ละครั้งผ่านทางเดียวเท่านั้น: ใบเสนอราคาของ MCP หรือหน้าต่างราคาของแอป
- เบราว์เซอร์อย่างเดียว (ไม่มีคีย์ API) ใช้หน้า Yoh ผ่านเบราว์เซอร์ ตรวจสอบราคาการสร้างในหน้าต่างยืนยันของแอป การสนทนาแบบข้อความกับผู้ช่วยอาจคิดค่าบริการทันที จึงต้องตกลงค่าใช้จ่ายก่อนใช้ อย่าป้อนข้อมูลการชำระเงินหรือรหัสเข้าสู่ระบบ
- MCP อย่างเดียว (ไม่ใช้เบราว์เซอร์) สำหรับสคริปต์ CI และเอเจนซี: เครื่องมือ ★ ใช้ใบเสนอราคา → confirmToken ตรวจสอบ get_job / get_run หยุดด้วย cancel_generation และดาวน์โหลดวิดีโอที่เสร็จแล้ว ask_assistant คิดค่ารอบข้อความทันที การสร้างตามกำหนดเวลาและการชำระเงินภายนอกมีกฎความยินยอมแยกกัน การเรียก MCP ไม่ต้องใช้เบราว์เซอร์
ตัวอย่างในทุกแบบ: โฆษณา 16 วินาที 9:16 ภาษาอังกฤษและญี่ปุ่น เขียนในเรื่องพร้อมสินค้า สร้างวิดีโอ ทำเวอร์ชันภาษาญี่ปุ่น แล้วเรนเดอร์สองไฟล์
เชื่อมต่อ
แนะนำ: ลงชื่อเข้าใช้ด้วย Yoh ไม่ต้องใช้คีย์ API ใช้ที่อยู่นี้ให้ตรงทุกตัวอักษร (yoh.app ที่ไม่มี www จะถูกส่งต่อ และการลงชื่อเข้าใช้จะตรวจที่อยู่) ใน Claude Code:
claude mcp add --transport http --scope user yoh https://www.yoh.app/api/mcpจากนั้นพิมพ์ /mcp เลือก yoh แล้วเลือก Authenticate ลงชื่อเข้าใช้ Yoh ในเบราว์เซอร์ที่เปิดขึ้นด้วยบัญชีที่ต้องการใช้ แล้วกดอนุมัติ
ใน Codex (CLI, ส่วนขยาย IDE หรือแอป ChatGPT บนเดสก์ท็อป) เพิ่มเซิร์ฟเวอร์แล้วลงชื่อเข้าใช้ จากนั้นตั้ง tool_timeout_sec = 300 ใต้ [mcp_servers.yoh] ใน ~/.codex/config.toml แล้วเปิด Codex ใหม่:
codex mcp add yoh --url https://www.yoh.app/api/mcp
codex mcp login yoh- แอป Claude บนเดสก์ท็อปหรือ claude.ai: Customize → Connectors → + Add → Add custom connector ตั้งชื่อว่า Yoh วางที่อยู่ แล้วกด Connect และลงชื่อเข้าใช้ เปิดใช้ในแต่ละแชตจาก + → Connectors ในแผน Team หรือ Enterprise เจ้าของต้องเพิ่มก่อน
- Cursor: เพิ่ม yoh ด้วยที่อยู่นี้ลงใน ~/.cursor/mcp.json แล้วลงชื่อเข้าใช้เมื่อ Cursor ขอ
- ChatGPT: Plugins → + → Add custom MCP server ตั้งชื่อว่า Yoh วางที่อยู่ แล้วลงชื่อเข้าใช้ บางแผนต้องให้ผู้ดูแลอนุญาตเซิร์ฟเวอร์ที่กำหนดเองก่อน
ตรวจสอบ: ให้เอเจนต์รัน get_credits ถ้าเห็นยอดเครดิต Yoh ของคุณแสดงว่าเชื่อมต่อแล้ว ดูและยกเลิกการเชื่อมต่อแอปได้ที่ การตั้งค่า → คีย์ API → แอปที่เชื่อมต่อ
เพื่อให้เอเจนต์เห็นหน้าที่มันแก้: Claude Code ใช้ claude --chrome (ส่วนขยาย Claude in Chrome) แอป Claude บนเดสก์ท็อปใช้คอนเนกเตอร์ Claude in Chrome หรือ Codex ในแอป ChatGPT บนเดสก์ท็อปใช้เบราว์เซอร์ของแอป สกิลจะบอกเอเจนต์ว่าเมื่อไรควรดู
หรือใช้คีย์ API (สคริปต์, CI, แอปที่ลงชื่อเข้าใช้ไม่ได้)
สร้าง API key ที่ /settings/api-keys ถ้าจะสร้างสตอรี่ใหม่และสร้างวิดีโอ ให้ใส่ stories:generate ด้วย ถ้าแค่แก้ไขโดยไม่ใช้เครดิต stories:read และ stories:write ก็เพียงพอ จากนั้นใน Claude Code:
claude mcp add --transport http yoh https://www.yoh.app/api/mcp \
--header "Authorization: Bearer sk_live_YOUR_API_KEY"หรือคอมมิตไฟล์ตั้งค่าโปรเจกต์ เพื่อให้ทั้งทีมได้การเชื่อมต่อทันทีที่เปิดรีโป โดยแต่ละคนใส่คีย์ของตัวเองผ่าน env:
{
"mcpServers": {
"yoh": {
"type": "http",
"url": "https://www.yoh.app/api/mcp",
"headers": { "Authorization": "Bearer ${YOH_API_KEY}" },
"timeout": 900000
}
}
}ใน Codex ให้เพิ่มเซิร์ฟเวอร์ใน ~/.codex/config.toml และตั้งค่า YOH_API_KEY ในเชลล์:
# 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ติดตั้งสกิล Yoh
สกิลนี้สอนเอเจนต์ของคุณให้สร้างและแก้ไขสตอรี่ Yoh ได้ดี: เริ่มในซีรีส์ ดูราคาก่อนใช้เครดิต และดูคลิปก่อนตัดสิน มีแค่ไฟล์เดียวและอัปเดตตามเซิร์ฟเวอร์เสมอ รันบรรทัดของเอเจนต์ที่คุณใช้ หรือ ดาวน์โหลด 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จากนั้นก็สั่งได้เลย: "ทำสตอรี่ 60 วินาทีเกี่ยวกับพยาบาลกะดึกที่ได้ยินเสียงผี" หรือ "กระชับจังหวะของตอนที่ 3 — ตัดช่วงเงียบออก เลื่อนคำบรรยายให้ตรงจังหวะ แล้วเรนเดอร์ใหม่" รัน /mcp ใน Claude Code หรือ Codex เพื่อตรวจสอบการเชื่อมต่อ
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.
เอเจนต์ทำอะไรได้บ้าง
| เครื่องมือ | ขอบเขต | ตัวอย่าง |
|---|---|---|
| อ่าน | stories:read | list_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 จะแนบภาพของคลิปมาด้วย (ถ้ามี) และเมื่อใส่ withFrames จะแนบเฟรมจริงที่สุ่มจากวิดีโอที่สร้างแล้ว ซึ่งเป็นทางเดียวที่เอเจนต์จะเห็นว่าคลิปหน้าตาเป็นอย่างไรจริง ๆ ส่วน get_asset_images ทำแบบเดียวกันกับตัวละคร ฉาก และพร็อพ |
| Template versions | stories:write / stories:read / assets:read | create_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:generate | generate_template_versionQuote the run first, show the maximum credits, then confirm. Poll get_template_version for persisted progress and recovery. |
| สร้าง ★ | stories:generate | create_storyเขียนสตอรี่ใหม่ทั้งเรื่องจากพรอมต์ estimate_story ใช้ฟรีและตีราคาทั้งแผนล่วงหน้าได้ ซึ่งใบเสนอราคารายการย่อยทำไม่ได้ ควรเริ่มที่ scope="story" (ข้อความอย่างเดียว) แทน "full" ที่จะคิดค่าวิดีโอทันที |
| Start from a video | stories:write / stories:read / assets:write | upload_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:read | create_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. |
| ซีรีส์ | series:read / series:write assets:read / assets:write | list_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_mainชั้นที่อยู่เหนือสตอรี่เดียว: ตัวละคร ฉากหลัง และสไตล์ที่ใช้ร่วมกันข้ามตอน ผูกสตอรี่ใหม่ด้วย seriesId ของ create_story เพื่อสืบทอดทั้งหมด |
| คลิปและบทพูด | stories:write | update_clip · update_clips · set_all_clips · reorder_clips · add_clip · delete_clip · duplicate_clip · reorder_clip · set_clip_duration · select_clip_version · 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_settingsบทพูดที่ยาวเกินคลิปจะขยายความยาวของคลิปให้อัตโนมัติ การแก้ข้อความไม่เคยเปลี่ยนสื่อที่สร้างไปแล้ว update_clips แก้ไขหลายคลิปพร้อมกันโดยแต่ละคลิปมีค่าของตัวเอง ส่วน set_all_clips ใส่ค่าเดียวกันให้ทุกคลิปและปฏิเสธข้อความ เพราะคำบรรยายเดียวกันทั้งตอนจะทำให้เรื่องแบนราบ ทั้งคู่จะปฏิเสธทั้งชุดหากมีรายการใดผิดพลาด reorder_clips ตั้งลำดับทั้งเรื่องได้ในครั้งเดียว |
| ผู้ช่วยของ Yoh | stories:write | ask_assistant · apply_assistant · undo_assistantส่งคำขอให้ผู้ช่วยของ Yoh เอง ตัวเดียวกับถาม yoh ในหน้าบทและเอเจนต์ AI ในตัวแก้ไข ask_assistant แค่เสนอเท่านั้น การแก้ไขจะกลับมาเป็นรายการเปลี่ยนแปลง และขั้นตอนที่มีค่าใช้จ่ายจะกลับมาเป็นใบเสนอราคา apply_assistant จะทำตามการเปลี่ยนแปลงที่คุณเก็บไว้ ส่วนขั้นตอนที่มีค่าใช้จ่ายต้องใช้ confirmToken ของใบเสนอราคานั้นหลังผู้ใช้ยอมรับด้วย undo_assistant จะคืนค่าสิ่งที่การทำงานที่นำไปใช้แล้วได้เปลี่ยนไป ask_assistant แต่ละครั้งใช้เครดิตเล็กน้อย: ตามค่า AI จริง ไม่เกินราคาประเมินเล็ก ๆ ต่อครั้ง |
| ไทม์ไลน์ | stories:write | edit_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_soundtrackโหมด Clip แสดงโครงสร้างและตำแหน่งต้นฉบับ ส่วนโหมด Edit บันทึกการแก้ไขแยกต่างหาก ทั้งสองมีประเภทเลเยอร์เหมือนกัน รวมถึงเสียงพูดและเพลง แต่ตำแหน่งและผลเรนเดอร์อาจต่างกัน ใช้ get_edit_timeline ก่อน edit_timeline การแก้ไขไม่ต้องส่งออกหรือใช้ Workflow เมื่อเรนเดอร์ ให้เลือก Clip หรือ Edit และระบุ timelineMode ใน render_story อย่างชัดเจน |
| เลเยอร์ | stories:write | add_overlay · update_overlay · move_overlay · resize_overlay · split_overlay · duplicate_overlay · delete_overlay · delete_freeform_row · upload_mediaเครื่องมือเลเยอร์ที่อ้างอิงด้วย ID ตัวเลขเหล่านี้ยังใช้กฎแบบเดิม: เลเยอร์สองอันอยู่บนแถวเดียวกันไม่ได้ (ตำแหน่งที่ชนกันจะถูกปฏิเสธพร้อมบอกแถวที่ว่าง) และเวลาจะปัดเป็นช่วง 0.1 วินาที หากต้องการให้ซ้อนทับกันหรือแก้ไขให้ตรงระดับเฟรม ให้ใช้ get_edit_timeline + edit_timeline สื่อต้องมี https ที่โฮสต์ไว้แล้ว |
| สไตล์ | stories:write | apply_overlay_preset · set_caption_styleพรีเซ็ตข้อความ เทมเพลตคำบรรยาย และแอนิเมชันเข้า-ออกราว 32 แบบของตัวแก้ไขเอง เพื่อให้เอเจนต์ใช้สิ่งที่ผลิตภัณฑ์มีอยู่จริงแทนการคิด CSS ขึ้นเอง สไตล์คำบรรยายกำหนดแยกทีละบรรทัดบทพูด |
| ตัวละครและฉาก | stories:write | add_character · update_character · delete_character · add_scene · update_scene · delete_scene · add_item · update_item · delete_item · keep_character_sheetการลบตัวละครจะนำมันออกจากทุกคลิป บทพูดของมันยังอยู่แต่ไม่มีผู้พูด จนกว่าคุณจะเลือกว่าใครพูด (update_dialogue_line) บทที่มีใบหน้าแบบมีลิขสิทธิ์หรือมีการจ้างที่รออยู่จะลบไม่ได้ |
| กำหนดเสียง | stories:write | set_character_voiceเสียงสำเร็จรูปที่กรองตามภาษาของสตอรี่ รวมกับเสียงโคลนที่พร้อมใช้ในบัญชี การกำหนดเสียงไม่มีค่าใช้จ่าย และบทพูดที่พากย์ไปแล้วจะคงเสียงเดิมที่ใช้ตอนสร้าง |
| พากย์ใหม่ | stories:write | set_revoice_enabled · select_revoice_take · undo_revoice · fix_audio_syncการสลับระหว่างเสียงอ่านใหม่กับการแสดงต้นฉบับไม่มีค่าใช้จ่ายและย้อนกลับได้ เก็บไว้ทั้งสองเทค |
| การสร้าง ★ | stories:generate | regenerate_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_storyแต่ละคู่คือ 'แก้' กับ 'วาดใหม่': edit_clip_image เขียนทับเฟรมปัจจุบันตามคำสั่ง ส่วน regenerate_clip_image วาดขึ้นใหม่ทั้งภาพ; edit_clip_video เป็นวิดีโอสู่วิดีโอที่รักษาตัวแบบ องค์ประกอบ และการเคลื่อนไหวไว้ ส่วน generate_clip_video สร้างใหม่จากข้อมูลอ้างอิงของคลิป (ชีตตัวละคร ฉากหลัง พร็อพ และภาพของคลิปถ้ามี) enhance_clip_image ดึงใบหน้าให้กลับมาเหมือนตามชีตตัวละคร regenerate_clip ทำภาพแล้วต่อด้วยวิดีโอในงานเดียว เหมาะที่สุดหลังแก้คำบรรยายหรือตัวละครของคลิป generate_clip_grid_reference วาดชีตความต่อเนื่องโดยใช้ช่องที่มาจากจังหวะเวลาของคลิปเอง ไม่ใช่ชุดเหตุการณ์ทั่วไปที่ตายตัว |
| Stop | stories:write | cancel_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 looks | assets:read stories:write | list_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 episodes | stories:write | plan_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 episodes | stories:read | get_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:generate | bulk_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 templates | stories:read | list_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 templates | stories:write | claim_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 team | series:write | reorder_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 team | stories:write | archive_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 team | series:read | get_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 audio | stories:write | upload_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 audio | stories:read | get_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:generate | check_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 clips | stories:write | apply_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:generate | check_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 looks | stories:read | list_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 looks | stories:write | use_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:generate | edit_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 voices | stories:write | create_custom_voice · delete_custom_voiceCreate or remove a custom voice using authorized reference audio; this does not regenerate existing dialogue. |
| Reuse library assets | assets:write | promote_asset_to_series · copy_series_assetsRead episode assets, promote them to the series library, or copy library assets to another series. |
| Reuse library assets | series:read | list_episode_assetsRead episode assets, promote them to the series library, or copy library assets to another series. |
| Editor playback | stories:write | set_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. |
| Publishing | publish:write | get_publish_options · publish_story · unpublish_storyInspect get_publish_options, review the video and audience, then publish. |
| Social posting | social:publish | list_social_accounts · social_publish · get_social_postNeeds account and caption approval; poll get_social_post for the result. |
| Generate an overlay asset ★ | stories:generate | generate_layer_assetQuote generate_layer_asset first; inspect its result with get_layer_asset before using the asset on the timeline. |
| Generate an overlay asset | stories:read | get_layer_assetQuote generate_layer_asset first; inspect its result with get_layer_asset before using the asset on the timeline. |
| Creator hiring, jobs and messages | hires:read | search_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 messages | hires:write | send_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 campaigns | ads:read | list_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 campaigns | ads:write | create_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:generate | write_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 activity | stories:read | get_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 activity | stories:write | keep_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 activity | assets:read | search_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. |
การใช้เครดิตมีสองด่าน เครื่องมือ ★ จะส่งใบเสนอราคาเป็นเครดิตและ confirmToken ที่ลงนามแล้วก่อนใช้เครดิต แสดงราคาและดำเนินการต่อเมื่อได้รับความยินยอมอย่างชัดเจนเท่านั้น เครื่องมือเหล่านี้ต้องใช้ stories:generate แต่ไม่ได้หมายความว่าเครื่องมืออื่นฟรี: ask_assistant คิดค่ารอบข้อความทันทีและต้องใช้ stories:generate (คีย์ที่มีแค่ stories:write เรียกใช้ไม่ได้) การสร้างตามกำหนดเวลา การจ่ายเงินให้ครีเอเตอร์ และโฆษณา Meta มีกฎความยินยอมและค่าใช้จ่ายแยกกัน
มันคงความปลอดภัยและถูกต้องอย่างไร
- เซิร์ฟเวอร์บังคับใช้กฎของตัวแก้ไข (การเลื่อนแบบสไลด์ ขอบเขตการตัด เลนคำบรรยาย แถวเลเยอร์) และเมื่อผิดกฎจะบอกช่วงที่อนุญาต แทนที่จะปรับค่าให้เงียบ ๆ
- เอเจนต์อ่านกฎทั้งหมดจากรีซอร์ส yoh://guide/timeline และพรอมต์ /mcp__yoh__edit-story ให้ขั้นตอนการทำงานที่ปลอดภัย
- การสร้างเป็นแบบอะซิงโครนัส เครื่องมือส่วนใหญ่จะคืน runId แล้วเอเจนต์จะ poll ด้วย get_run บางตัว (create_story, render_story, cast art) คืน jobId สำหรับ get_job เหมือน REST API
- ตั้งค่า timeout ให้ยาวใน .mcp.json (เช่น 900000) เพื่อไม่ให้การเรนเดอร์หมดเวลาฝั่งไคลเอนต์
ตรวจสอบการเชื่อมต่อ
รีโปมีสโมกเทสต์มาให้ มันสื่อสารด้วยโปรโตคอลจริง ทดสอบกฎต่าง ๆ และพิสูจน์ว่าด่านการใช้เครดิตทำงานจริง โดยไม่ใช้เครดิตเลย และการเขียนเพียงอย่างเดียวที่ทำก็จะถูกย้อนกลับ:
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จัดการ API Key
endpoint การจัดการ API key ใช้ session authentication (คุกกี้ของผู้ใช้ที่ล็อกอินอยู่) ไม่ใช่ API key ใช้ API Keys page สำหรับขั้นตอนการทำงานผ่านเบราว์เซอร์ หรือเรียก endpoint เหล่านี้ผ่านโปรแกรมจากแบ็กเอนด์ของคุณด้วยเซสชันที่ถูกต้อง /settings/api-keys
รายการ API key
/api/v1/api-keysscope: session{
"data": [{
"id": "key_abc123", "name": "Production",
"keyPrefix": "sk_live_7WH3",
"scopes": ["stories:generate", "stories:read", "assets:read"],
"revokedAt": null, "createdAt": "2026-04-03T10:00:00.000Z"
}]
}สร้าง API key
/api/v1/api-keysscope: session// Response — full key shown once
{
"id": "key_abc123",
"key": "sk_live_...",
"name": "My integration",
"keyPrefix": "sk_live_7WH3",
"scopes": ["stories:generate", "stories:read", "assets:read"]
}เพิกถอน API key
/api/v1/api-keys/:idscope: session{ "id": "key_abc123", "revoked": true }ข้อผิดพลาด
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.
เอนด์พอยต์ส่วนใหญ่ส่งคืนออบเจ็กต์ error พร้อม code ที่เครื่องอ่านได้ เอนด์พอยต์เฉพาะบางรายการอาจส่งคืนข้อผิดพลาดแบบข้อความหรือรหัสระดับบนสุด โปรดตรวจสอบสถานะ 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" } }| HTTP | code | ความหมาย |
|---|---|---|
| 401 | unauthorized | ไม่มี API key หรือ API key ไม่ถูกต้อง |
| 403 | forbidden | API key ไม่มีขอบเขตสิทธิ์ที่จำเป็น |
| 404 | not_found | ไม่พบทรัพยากรที่ร้องขอ |
| 400 | bad_request | เนื้อหาคำขอหรือพารามิเตอร์ไม่ถูกต้อง |
| 402 | insufficient_credits | เครดิตไม่เพียงพอสำหรับดำเนินการนี้ |
| 429 | rate_limited | คำขอมากเกินไป ตรวจสอบส่วนหัว Retry-After |
| 500 | internal_error | เกิดข้อผิดพลาดของเซิร์ฟเวอร์ที่ไม่คาดคิด |
ขีดจำกัดอัตราการเรียก: endpoint การสร้างอนุญาต 5 คำขอต่อนาทีต่อ API key การอ่านข้อมูลและการแก้ไขคลิป (ซึ่งไม่ใช้เครดิต) อนุญาต 60 คำขอ เมื่อคุณถึงขีดจำกัด การตอบกลับจะมีส่วนหัว Retry-After ระบุจำนวนวินาทีที่ต้องรอ งานที่ล้มเหลว (หลังจากลองใหม่ครบทุกครั้ง) จะคืนเครดิตที่ถูกหักไปโดยอัตโนมัติ
เผยแพร่ลงโซเชียล
ส่งวิดีโอที่เสร็จแล้วไปยังบัญชีที่เชื่อมต่อไว้ได้ทันที ระบบอัตโนมัติจึงขับเคลื่อน สร้าง → เรนเดอร์ → เผยแพร่ ได้ด้วยข้อมูลรับรองชุดเดียว โปรดเชื่อมต่อบัญชีในแอปก่อน เอนด์พอยต์นี้จะเผยแพร่ไปยังบัญชีเหล่านั้น
เผยแพร่วิดีโอ
/api/v1/social/publishscope: social:publishคืนค่า 202 พร้อม { postId, status: "publishing" } และอัปโหลดอยู่เบื้องหลัง — ให้ poll ที่เอนด์พอยต์สถานะด้านล่าง หากต้องการให้รอจนอัปโหลดเสร็จแทน ให้ส่ง wait: true (อาจใช้เวลาหลายนาที) URL วิดีโอต้องตรงกับผลเรนเดอร์ที่สำเร็จหรือวิดีโอต้นฉบับที่อัปโหลดของเรื่องนี้ สตรีม YouTube อนุญาตเฉพาะที่อยู่สาธารณะและใช้ขีดจำกัดการอัปโหลดเดิมที่ 512 MB
socialAccountIdstringstoryIdstringvideoUrlstringplatformstringcaptionstringyoutubeobjectdurationSecnumberaspectRatiostringfileSizeBytesnumberwaitbooleanตรวจสอบสถานะการเผยแพร่
/api/v1/social/publish/:postIdscope: social:publishวงจรสถานะ:
publishing→published|failed.