API-Referenz
KI-Videos programmatisch generieren. Prompt senden, Status abfragen und eine direkte Video-URL erhalten — alles via REST.
Base URL: https://www.yoh.appCode-Beispiele verwenden den Platzhalter sk_live_YOUR_API_KEY, bis du dich anmeldest.
Schnellstart
Erstelle dein erstes Video in drei API-Aufrufen: generieren → abfragen → herunterladen.
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
}Das ist alles. Die videoUrl ist eine öffentlich zugängliche URL, die du sofort herunterladen, streamen oder teilen kannst.
Authentifizierung
Alle API-Anfragen müssen deinen API-Key im Authorization-Header mit dem Bearer-Schema enthalten.
Authorization: Bearer sk_live_YOUR_API_KEYAPI-Keys beginnen mit sk_live_. Du kannst Keys auf der API Keys page erstellen und verwalten. /api-keys
Halte deine API-Keys geheim. Veröffentliche sie nicht in clientseitigem Code oder öffentlichen Repositories. Falls ein Key kompromittiert wurde, widerrufe ihn sofort auf der Seite „API-Keys“.
Scopes & Credits
Jedem API-Key sind ein oder mehrere Scopes zugewiesen, die steuern, auf welche Endpunkte er zugreifen kann. Die Textgenerierung für eine Story kostet 55 Credits pro Job; Scopes, die zusätzlich Clips oder ein finales Rendern erzeugen (einschließlich des Standard-Scopes „full“), berechnen diese vorab zusätzlich. Credits werden erstattet, falls der Job nach allen Versuchen fehlschlägt.
| Scope | Gewährt Zugriff auf |
|---|---|
| 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. Spends no credits, so a key with this scope but WITHOUT stories:generate can edit a story and physically cannot pay to generate anything. Implied by stories:generate; implies assets:write. |
| assets:read | GET .../characters, .../scenes, .../items, .../clips, GET .../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. |
Ein Video generieren
Einen Job zur Videogenerierung einreihen
/api/v1/stories/generatescope: stories:generateÜbermittelt einen Prompt und gibt sofort eine jobId zurück. Das Video wird asynchron generiert. Frage GET /api/v1/stories/jobs/:jobId ab, um den Fortschritt zu verfolgen.
Request-Body
| Feld | Typ | Status | Beschreibung |
|---|---|---|---|
prompt | string | erforderlich | Beschreibung der Story, max. 5000 Zeichen. |
aspectRatio | string | optional | Einer von 16:9, 9:16, 1:1, 21:9. Standard: 16:9. |
renderVideo | boolean | optional | false — gibt den ersten AI-Clip zurück (schnell, ~10 Min.). true — rendert alle Clips via Remotion zu einer fertigen MP4 (~12–15 Min.). |
scope | string | optional | 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 | optional | Anzeigename für die Story. |
contentType | string | optional | Kategorie des Story-Formats. Einer von cinematic_story, news_analysis, persona_channel, ad_spot. Standard: cinematic_story. |
locale | string | optional | Sprachcode, z. B. en, zh-TW. Standard: en. |
seriesId | string | optional | An eine bestehende Story-Reihe anhängen, um ein konsistentes Aussehen der Charaktere zu gewährleisten. |
assets | array | optional | 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 | optional | 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 | optional | Implied by continuityMode: "finale"; you rarely need both. |
allowMultiStory | boolean | optional | 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 | optional | 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[] optional
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.
| Feld | Typ | Beschreibung |
|---|---|---|
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
Optional nested object controlling duration, output format, and model selection. Every field has a default; pass only what you want to override. The model fields (videoModel, imageModel, characterImageModel, modelStrategy) may also be passed at the top level as a convenience. Unknown model ids degrade to auto rather than failing the request.
This is a superset of the studio's own controls, not a mirror of them. resolution 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 and MiniMax H3 from 720p). 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.
| Feld | Typ | Werte | Standard |
|---|---|---|---|
storyInputMode | string | auto · develop · follow_script (auto decides whether the prompt is an idea to develop or a finished script to follow verbatim) | 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) | 60 |
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 | 16:9 |
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 | storyboard frames + 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 or bytedance/seedream-5-pro (list_models: character) | 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
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 | Felder |
|---|---|
| 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. |
Andere optionale Felder
| Feld | Typ | Beschreibung |
|---|---|---|
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"
}
}'Antwort 202 Accepted
{ "jobId": "cmnk8wu2q0001q3vpmqv93nmy", "status": "queued", "pollUrl": "/api/v1/stories/jobs/..." }Schritt-für-Schritt-Endpunkte
After generating a story with scope: "story", use these endpoints to generate storyboard images, clip videos, and the final rendered MP4 as separate steps.
/api/v1/stories/:storyId/assetsGenerate the cast and world: character portraits + 2×2 character sheets, plus scene and item images. Run this before /storyboard for the best face consistency. Skips assets that already have images. 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 }.
/api/v1/stories/:storyId/storyboardGenerate storyboard grid images for all clips. Skips clips that already have images. Runs as the app's missing-stills batch (one run per clip, each refunded on its own if it fails): answers 202 { runId, runUrl, clipsToGenerate, estimatedCredits }; poll GET /api/runs/:runId.
/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.
/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.
Ein Clip nach dem anderen
The batch endpoints above only fill in what's missing — /storyboard skips clips that already have an image and /videos skips clips that already have a video. To re-roll a shot you don't like, target it directly. These are the same operations as the editor's per-clip regenerate buttons, and they overwrite whatever that clip already has.
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 batch clips run one at a time in story order, each continuing the previous clip's video; "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. 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), 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, or CLIP_VIDEO_IN_PROGRESS).
/api/v1/stories/:storyId/clips/:clipId/image(Re)generate the storyboard image for ONE clip. Optional body: prompt (art direction for this shot) and imageModel. 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 storyboard 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. 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 2×2 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/: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) 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. Without placement, a version is saved on its own, linked to its episode, and a plain copy of an episode in a story joins it as the next episode ("series" files it as the next episode anyway). 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), 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 replacedStory bearbeiten
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 (its dialogue fitted to the delivered video), the clip panel's own read and actions. GET answers 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/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: Generate storyboard images (~3-5 min)
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 → videoUrlJob-Status abfragen
Job-Status abrufen
/api/v1/stories/jobs/:jobIdscope: stories:readFrage diesen Endpunkt nach dem Aufruf von POST /api/v1/stories/generate ab. Der Job dauert normalerweise 8–15 Minuten, abhängig von der Länge der Story und davon, ob renderVideo aktiviert ist. Empfohlenes Abfrageintervall: alle 15–30 Sekunden.
Lebenszyklus des Job-Status
queuedgenerating_storygenerating_assetsgenerating_videosrendering_videocompletedoderfailedWhich 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". A 402 insufficient_credits carries required, balance and topUpUrl.
Antwortfelder
| Feld | Typ | Beschreibung |
|---|---|---|
jobId | string | Eindeutige Job-ID. |
status | string | Aktueller Status. Siehe Lebenszyklus oben. |
progress | number | Ganzzahl von 0–100, die den Fortschritt in Prozent angibt. |
storyId | string|null | Wird gesetzt, sobald die Story-Generierung abgeschlossen ist. Nutze sie, um Assets abzurufen. |
renderVideo | boolean | Gibt an, ob eine fertig gerenderte MP4 angefordert wurde. |
videoUrl | string|null | Absolute Video-URL, wenn der Status „completed“ ist. Entweder eine gerenderte MP4 (renderVideo: true) oder das AI-Video des ersten Clips (renderVideo: false). |
error | string|null | Lesbare Fehlermeldung. Wird bei Fehlern gesetzt — und auch während ein Job wiederholt wird, wobei der Status zurück auf „queued“ springt. Bei Erfolg immer null. |
waitingFor / message | string (optional) | Vorhanden, während ein Job auf das Ergebnis eines Providers wartet (Status bleibt „queued“, Fehler ist null): Der Wert ist „provider“ und die Nachricht gibt an, worauf gewartet wird. Frage weiter ab. |
execution | object (optional) | Der Workflow-Run, zu dem der Job gehört: { version, runId, phase, heartbeatAt, status }. Verfolge ihn unter GET /api/runs/:runId (oder dessen /stream). Bei älteren Jobs nicht vorhanden. |
createdAt / startedAt / completedAt | string (ISO) | Zeitstempel. |
runId / runUrl | string (202 only) | In der 202-Antwort bei jedem Job-Start (nicht bei dieser Abfrage): Der für den Job gestartete Run und seine Status-URL. Optional nutzbar; das Abfragen dieses Endpunkts reicht aus. |
Storys
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.
Storys auflisten
/api/v1/storiesscope: stories:readGibt eine paginierte Liste deiner Storys zurück.
Query-Parameter
| Parameter | Standard | Beschreibung |
|---|---|---|
page | 1 | Seitenzahl (≥ 1). |
limit | 20 | Ergebnisse pro Seite (1–100). Capped at 100. |
status | — | Nach Story-Status filtern. |
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 }
}Eine Story abrufen
/api/v1/stories/:storyIdscope: stories:readGibt vollständige Metadaten für eine einzelne Story zurück, einschließlich der Anzahl ihrer Charaktere, Szenen, Items und Clips.
{
"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"
}Asset-Übersicht
Nach Abschluss eines Jobs kannst du alle für die Story generierten Assets über die vom Job zurückgegebene storyId abrufen. Alle Asset-Endpunkte erfordern den Scope assets:read.
GET /api/v1/stories/:id/charactersCharaktere — Name, Beschreibung, Rolle, imageUrl, Stimminformationen
GET /api/v1/stories/:id/scenesSzenen — Beschreibung, Tageszeit, Wetter, Lichtverhältnisse, imageUrl
GET /api/v1/stories/:id/itemsRequisiten / Items — Beschreibung, Bedeutung, imageUrl
GET /api/v1/stories/:id/clipsClips — Narration, Dauer, videoUrl, imageUrl, audioUrl
Charaktere
Charaktere auflisten
/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
}]
}Clips & Videos
Clips auflisten
/api/v1/stories/:storyId/clipsscope: assets:readJeder Clip ist ein Szenensegment mit eigenem AI-generiertem Video, Standbild und Audio. Alle URL-Felder sind absolut und öffentlich zugänglich.
{
"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"
}]
}Wenn renderVideo false ist, verweist die videoUrl des Jobs auf das Video des ersten Clips. Nutze diesen Endpunkt, um die Video-URL für jeden einzelnen Clip abzurufen.
Story-Asset-Library
Ein Asset einmal hochladen, für immer referenzieren
/api/v1/series/:seriesId/assetsscope: assets:writeDieselbe Library, in die auch das Asset-Panel im Studio schreibt. Lade einen Charakter, einen Hintergrund, ein Item oder eine Style-Referenz hoch, behalte die zurückgegebene ID und referenziere sie bei der Generierung, indem du diese ID als seriesAssetId im Assets-Array übergibst – so musst du nicht für jedes Video einen neuen Bild-Link erstellen. Von dir übermittelte Bilder werden auf unserem Speicher neu gehostet, sodass die ID auch nach Ablauf deiner eigenen Upload-URL gültig bleibt.
| Feld | Typ | Beschreibung |
|---|---|---|
type | string | character, scene, item oder reference. |
name | string | Anzeigename. Auch der Key, mit dem spätere Generierungen abgeglichen werden. |
imageUrl | string | Öffentliche URL des Bildes. Alternativ imageBase64 senden. |
itemType | string | Nur Items: prop, product oder logo. Ein Logo wird flach eingebunden und erhält keine 3D-Tiefe. |
isHeroProduct | boolean | Nur Items: das Produkt, um das es in dieser Anzeige geht. Es erscheint in jedem Clip. |
isPrimaryBackground | boolean | Nur Szenen: das Studio/Set, das in jedem Anchor-Shot wiederverwendet wird. |
seriesDuplicate | string | Nur Charaktere, nach Rücksprache mit dem User: „new“ fügt einen zweiten Charakter mit einem Namen hinzu, den die Story bereits hat. |
- Sende ein einzelnes Asset oder ein „assets“-Array, um bis zu 100 gleichzeitig zu aktualisieren oder einzufügen.
- Standardmäßig werden Assets bei exakter Namensübereinstimmung zusammengeführt, sodass das erneute Hochladen desselben Katalogs idempotent ist – außer bei einem Charakter, dessen Namen die Story bereits hat: Das führt zu 409 SERIES_NAME_EXISTS mit dem vorhandenen Charakter, es wird nichts gespeichert. Frage den User, sende dann seriesDuplicate "new", um einen zweiten hinzuzufügen, oder übergib ?mergeStrategy=exact, um den bestehenden zu aktualisieren. ?mergeStrategy=fuzzy ist tolerant gegenüber Umbenennungen; insert erstellt immer ein neues Asset.
- DELETE antwortet mit 409 SERIES_ASSET_IN_USE und den Episoden, die das Asset noch verwenden (Storys: ID, Titel, Clip-Anzahl), und löscht nichts. Frage den User, sende dann confirm: true (Body oder ?confirm=true): Episoden behalten ihre eigene Kopie und ihre Clips ändern sich nicht. Ein Charakter in einem offenen Casting-Vorgang antwortet mit 409 SERIES_ASSET_HIRED (Hires) und kann erst entfernt werden, wenn das Besetzen beendet ist.
- Setze isHeroProduct bei einem Item (ad_spot) oder isPrimaryBackground bei einer Szene (news_analysis / persona_channel), um sie zu pinnen. Bei der Generierung wird der Pin automatisch aufgelöst – du musst die heroItemId nie manuell schreiben.
| Scope | Gewährt Zugriff auf |
|---|---|
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.
Story-Assets
Ein Charakterbild für eine Story registrieren
/api/v1/series-assets/ensure-character-imagescope: assets:writePOST /api/v1/series-assets/ensure-character-image funktioniert weiterhin unverändert, verarbeitet aber nur Charaktere. Nutze bevorzugt diesen Endpunkt.
Lädt ein Charakter-Referenzbild für eine Story hoch und speichert es, damit die AI ein konsistentes Gesicht und einen konsistenten Look über alle Episoden dieser Story hinweg verwendet. Dieser Vorgang ist idempotent – ihn mehrfach mit demselben Charakternamen aufzurufen, ist sicher.
| Feld | Typ | Beschreibung |
|---|---|---|
seriesId | string | ID der Story, der der Charakter hinzugefügt werden soll. |
characterName | string | Der Name des Charakters (wird als eindeutiger Schlüssel innerhalb der Story verwendet). |
imageUrl | string | Öffentlich zugängliche URL des Referenzbildes für den Charakter. |
Verwaltete Konten
Für Plattformen, die viele Kunden bedienen. Anstatt eines Keys und eines Credit-Pools für alle – wo ein aktiver Kunde das Guthaben für den Rest aufbraucht und niemand sagen kann, wer was ausgegeben hat – kannst du pro Kunde ein Konto bereitstellen und jedes aus deinem eigenen Guthaben finanzieren. Deine Kunden melden sich nie hier an oder bezahlen uns; die Geschäftsbeziehung bleibt bei dir.
Ein Konto bereitstellen
/api/v1/accountsscope: accounts:manageexternalId ist deine eigene ID für den Kunden. Ein zweifacher Aufruf mit derselben ID gibt das bestehende Konto zurück, anstatt ein zweites zu erstellen, sodass ein wiederholter Request sicher ist. Der API-Key wird einmalig bei der ersten Erstellung zurückgegeben – wir speichern einen Hash und können ihn nicht erneut anzeigen, also speichere ihn dauerhaft, wenn du ihn erhältst. Ein neues Konto beginnt mit einem Guthaben von Null.
| Feld | Typ | Beschreibung |
|---|---|---|
externalId | string | Erforderlich. Deine ID für diesen Kunden – zum Beispiel eine Tenant-ID. Eindeutig pro Plattform. |
name | string | Optionales Label für dein eigenes Reporting. |
Key eines verwalteten Kontos ersetzen
/api/v1/accounts/:accountId/api-keysscope: accounts:manageWenn der ursprüngliche Key verloren ging, rufe diesen Endpunkt mit dem Plattform-Key des Eigentümers und ohne Request-Body auf. Verwende die Account-ID oder externalId. Er widerruft atomar die vorherigen Keys des Kontos und gibt einmalig einen neuen apiKey zurück (201). Speichere ihn sicher. Ein wiederholter Aufruf rotiert den Key erneut; normale Wiederholungsversuche bei der Kontobereitstellung ersetzen niemals einen funktionierenden Key.
Ein Konto finanzieren
/api/v1/accounts/:accountId/creditsscope: accounts:manageÜberträgt Credits von deinem eigenen Guthaben auf das des Kontos – ein Transfer, keine Gewährung, du kannst also nur das weitergeben, was du tatsächlich gekauft hast. Gibt 402 zurück, wenn dein eigenes Guthaben dies nicht deckt. :accountId akzeptiert entweder unsere Account-ID oder deine externalId.
| Feld | Typ | Beschreibung |
|---|---|---|
amount | integer | Erforderlich. Zu übertragende Credits. Muss positiv sein. |
idempotencyKey | string | Übergib deine Rechnungs- oder Abo-Zeitraum-ID. Ein wiederholter oder erneut zugestellter Aufruf gibt dann den ursprünglichen Transfer zurück, anstatt das Konto doppelt zu finanzieren. Versuche es mit demselben Key, Empfänger und Betrag erneut. Eine Wiederholung überträgt keine Credits erneut, selbst wenn das Plattform-Guthaben inzwischen aufgebraucht ist. Das Ändern des Empfängers oder Betrags gibt 409 zurück. Zu große Keys werden abgelehnt statt gekürzt. |
description | string | Optionale Notiz, die auf beiden Seiten des Transfers im Credit-Verlauf angezeigt wird. |
GET /api/v1/accounts listet deine Konten mit dem jeweiligen Guthaben und den Gesamtausgaben auf; GET /api/v1/accounts/:accountId/credits liest eines aus. Der eigene Key eines verwalteten Kontos verfügt nur über Berechtigungen zum Generieren – er kann niemals selbst Konten bereitstellen oder finanzieren.
Claude Code & Codex (MCP)
Yoh liefert einen MCP-Server aus, damit AI-Agenten (Claude Code, Codex, Claude Desktop oder jeder MCP-Client) Storys in deinem Account erstellen und bearbeiten können: schreibe eine neue Story aus einer Idee, füge ihre Charaktere hinzu und generiere das Video, oder ändere Clip-Inhalte und Dialoge, Timeline-Verschiebungen und Kürzungen sowie freiform-Overlays. Alles, was Credits kostet, wartet jedes Mal auf deine ausdrückliche Zustimmung. Es gelten dieselben Regeln wie im Browser-Editor, sodass eine Bearbeitung aus dem Terminal genau dort landet, wo auch ein Drag landen würde.
Drei Arten der Nutzung
Wähle den Modus, der zu den Tools deines Agents passt. Der Yoh-Skill unten deckt alle drei ab.
- MCP + Browser (empfohlen in Codex, Claude Code und den Claude-Apps) Der Agent arbeitet über diese MCP-Tools und steuert daneben einen Browser-Tab (Claude in Chrome, Codex-Browser): Nach jedem Schritt öffnet er den enthaltenen Link (die Skript-Tabelle nach einer neuen Story, das Clip-Panel nach einem Clip-Video, die „Alle generieren“-Liste während eines Durchlaufs, den erweiterten Editor für Timeline und Textebenen, die MP4 des Renders) und prüft das Ergebnis auf der echten Seite. Melde dich in diesem Browser einmal mit demselben Account wie für den API-Key an. MCP und die Seite teilen sich dieselben Daten: Die Episoden-Seite aktualisiert sich innerhalb von Sekunden nach einer MCP-Änderung, und der Agent lädt den erweiterten Editor nach einer Timeline-Änderung neu. Jede Ausgabe erfolgt nur über einen Pfad: das MCP-Angebot oder den Preisdialog der App.
- Nur Browser (kein API-Key) Der Agent bedient die yoh.app-Seiten in deinem Browser so, wie du es tun würdest: Erstellen → Story, die Skript-Tabelle und das Clip-Panel, der Charakter-Editor und dessen Modellauswahl, Alle generieren, der erweiterte Editor, Versionen und Export. Jede Ausgabe erfolgt über den app-eigenen Preisdialog, und der Agent gibt niemals Zahlungsdaten oder deinen Anmeldecode ein.
- Nur MCP (Headless) Für Skripte, CI und Agenturen, die viele Varianten erstellen: quote → confirmToken für jede Ausgabe, Abfrage von get_job / get_run, Stoppen mit cancel_generation, Erstellen von Varianten mit create_story_versions und generate_all_clips sowie Download der videoUrl jedes fertigen Renders. Kein Browser erforderlich.
Beispiel in jedem Modus: Eine 16-sekündige 9:16-Anzeige auf Englisch und Japanisch, geschrieben als Story mit ihrem Produkt, generiert, als japanische Version angelegt und zweimal gerendert.
Verbinden
Empfohlen: Mit Yoh anmelden, kein API-Schlüssel nötig. Verwende genau diese Adresse (yoh.app ohne www leitet weiter, und die Anmeldung prüft die Adresse). In Claude Code:
claude mcp add --transport http --scope user yoh https://www.yoh.app/api/mcpGib dann /mcp ein, wähle yoh, dann Authenticate, melde dich im geöffneten Browser mit dem gewünschten Konto bei Yoh an und bestätige.
In Codex (CLI, IDE-Erweiterung oder ChatGPT-Desktop-App) den Server hinzufügen und anmelden, dann unter [mcp_servers.yoh] in ~/.codex/config.toml tool_timeout_sec = 300 setzen und Codex neu starten:
codex mcp add yoh --url https://www.yoh.app/api/mcp
codex mcp login yoh- Claude-Desktop-App oder claude.ai: Customize → Connectors → + Add → Add custom connector, Name Yoh, Adresse einfügen, dann Connect und anmelden. In jedem Chat über + → Connectors einschalten. Bei Team- oder Enterprise-Plänen fügt ihn zuerst ein Owner hinzu.
- Cursor: yoh mit dieser Adresse in ~/.cursor/mcp.json eintragen und anmelden, wenn Cursor fragt.
- ChatGPT: Plugins → + → Add custom MCP server, Name Yoh, Adresse einfügen und anmelden. Bei manchen Plänen muss ein Admin eigene Server erst erlauben.
Prüfen: Lass deinen Agenten get_credits ausführen; er sollte dein Yoh-Guthaben zeigen. Verbundene Apps siehst und trennst du unter Einstellungen → API-Schlüssel → Verbundene Apps.
Damit dein Agent die Seiten sieht, die er ändert: Claude Code mit claude --chrome (Erweiterung Claude in Chrome), die Claude-Desktop-App mit dem Connector Claude in Chrome oder Codex in der ChatGPT-Desktop-App mit ihrem Browser. Der Skill sagt dem Agenten, wann er nachsehen soll.
Oder mit API-Schlüssel (Skripte, CI, Apps ohne Anmeldung)
Erstelle einen API-Key unter /api-keys. Um neue Storys zu erstellen und Videos zu generieren, schließe stories:generate ein; zum Bearbeiten ohne Kosten reichen stories:read und stories:write aus. Dann in Claude Code:
claude mcp add --transport http yoh https://www.yoh.app/api/mcp \
--header "Authorization: Bearer sk_live_YOUR_API_KEY"Oder hinterlege eine Projekt-Konfiguration, damit ein ganzes Team die Verbindung erhält, wenn es das Repo öffnet, wobei jedes Mitglied seinen eigenen Key via env bereitstellt:
{
"mcpServers": {
"yoh": {
"type": "http",
"url": "https://www.yoh.app/api/mcp",
"headers": { "Authorization": "Bearer ${YOH_API_KEY}" },
"timeout": 900000
}
}
}Füge in Codex den Server zu ~/.codex/config.toml hinzu und setze YOH_API_KEY in deiner Shell:
# 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 = 300Installiere den Yoh-Skill
Der Skill bringt deinem Agent bei, wie man eine Yoh-Story gut erstellt und bearbeitet: in einer Serie starten, Preise vor dem Ausgeben prüfen und die Clips ansehen, bevor man sie bewertet. Es ist eine einzelne Datei, die mit dem Server aktuell gehalten wird. Führe die Zeile für deinen Agent aus, oder SKILL.md herunterladen
# 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.mdFrage dann einfach: „Erstelle eine 60-sekündige Story über eine Nachtschwester, die Geister hören kann“ oder „Straffe das Pacing von Episode 3: kürze die Pausen, verschiebe die Untertitel auf die Beats und rendere neu“. Führe /mcp in Claude Code oder Codex aus, um die Verbindung zu prüfen.
Was der Agent tun kann
| Tools | Scope | Beispiele |
|---|---|---|
| Lesen | stories:read | list_stories · get_story · list_story_versions · get_timeline · 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 · estimate_storyget_clip fügt das Storyboard-Bild und, mit withFrames, echte Frames aus dem generierten Video bei – die einzige Möglichkeit für den Agent zu sehen, wie ein Clip tatsächlich aussieht. get_asset_images macht dasselbe für Cast, Sets und Requisiten. |
| 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. |
| Erstellen ★ | stories:generate | create_storySchreibe eine brandneue Story aus einem Prompt. estimate_story ist kostenlos und berechnet einen Preis für den gesamten Plan im Voraus, was Einzelangebote pro Aktion nicht können. Beginne mit scope="story" (nur Text) statt "full", was sofort Gebühren für Video berechnet. |
| Start from a video | stories:write / stories:read | 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. |
| Story-Verwaltung | 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_mainDie Ebene über einer Story: die gemeinsamen Charaktere, Hintergründe und der Look, die über Episoden hinweg bestehen bleiben. Füge eine neue Story mit create_story seriesId hinzu, um diese zu übernehmen. |
| Clips & Dialog | stories:write | update_clip · update_clips · set_all_clips · reorder_clips · add_clip · delete_clip · duplicate_clip · reorder_clip · set_clip_duration · select_clip_version · select_clip_image_version · add_dialogue_line · update_dialogue_line · remove_dialogue_line · hide_dialogue_line · add_audio_fx · update_audio_fx · remove_audio_fx · update_story · update_caption_settingsDialog, der über seinen Clip hinauswächst, verlängert die Dauer des Clips automatisch. Textänderungen ändern niemals bereits generierte Medien. update_clips bearbeitet viele Clips gleichzeitig, jeder mit eigenen Werten; set_all_clips setzt einen Wert für jeden Clip und lehnt Text ab, da dieselbe Beschreibung überall die Episode flach wirken lässt. Beide lehnen den gesamten Batch ab, wenn ein Eintrag fehlerhaft ist. reorder_clips legt eine komplette Reihenfolge in einem Aufruf fest. |
| Yoh-Assistent | stories:write | ask_assistant · apply_assistant · undo_assistantÜbergib eine Anfrage an den Yoh-eigenen Assistenten, der hinter „KI fragen“ auf der Skript-Seite und dem KI-Agent des Editors steht. ask_assistant macht nur Vorschläge: Änderungen kommen als Korrekturen und kostenpflichtige Schritte als Angebote zurück. apply_assistant führt die Änderungen aus, die du behältst; eine kostenpflichtige benötigt zusätzlich den confirmToken des Angebots, nachdem der User zugestimmt hat. undo_assistant macht rückgängig, was ein angewendeter Durchlauf geändert hat. Jeder ask_assistant-Turn kostet einige Credits: seine echten KI-Kosten, bis zu einem kleinen Angebot pro Turn. |
| Timeline | stories:write | move_clip · resize_clip · trim_clip · reset_clip_timing · close_gaps · set_clip_framing · move_caption · hide_clip_video · pin_dialogue_read · export_clip_to_freeform · export_caption_to_freeform · remove_soundtrackDas Verschieben eines Clips verschiebt jeden späteren Clip; das Ändern der Größe verankert den Start, sodass das Ziehen am Anfang ein Trimmen des Kopfes ist. Story-Elemente können nicht gelöscht oder geteilt werden – blende sie aus oder exportiere sie in ein freies Overlay. |
| Overlays | stories:write | add_overlay · update_overlay · move_overlay · resize_overlay · split_overlay · duplicate_overlay · delete_overlay · delete_freeform_row · upload_mediaEs dürfen sich keine zwei Overlays in einer Reihe überschneiden; eine kollidierende Platzierung wird abgelehnt und die freien Reihen werden aufgelistet. Medien benötigen eine gehostete https-Quelle. |
| Look | stories:write | apply_overlay_preset · set_caption_styleDie app-eigenen Text-Presets, Untertitel-Vorlagen und ca. 32 Ein-/Ausgangs-Animationen – so nutzt der Agent das, was das Produkt mitliefert, statt CSS zu erfinden. Das Styling der Untertitel erfolgt pro Dialogzeile. |
| Charaktere & Sets | stories:write | add_character · update_character · delete_character · add_scene · update_scene · delete_scene · add_item · update_item · delete_item · keep_character_sheetDas Löschen eines Charakters entfernt ihn aus jedem Clip. Seine Textzeilen bleiben ohne Sprecher erhalten, bis du entscheidest, wer sie spricht (update_dialogue_line); eine Rolle mit einem lizenzierten Gesicht oder einem ausstehenden Casting kann nicht gelöscht werden. |
| Stimmen-Casting | stories:write | set_character_voiceStock-Stimmen, gefiltert nach der Sprache der Story, plus die bereitstehenden geklonten Stimmen des Accounts. Casting ist kostenlos; bereits vertonter Dialog behält die Stimme, mit der er gerendert wurde. |
| Neu vertonen | stories:write | set_revoice_enabled · select_revoice_take · undo_revoice · fix_audio_syncDas Wechseln zwischen den neuen Aufnahmen und der ursprünglichen Performance ist kostenlos und reversibel; die Takes bleiben in jedem Fall erhalten. |
| Generierung ★ | 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_storyJedes Paar ist Bearbeitung vs. Neugenerierung: edit_clip_image schreibt den aktuellen Frame basierend auf einer Anweisung um, während regenerate_clip_image einen neuen zeichnet; edit_clip_video ist Video-zu-Video und behält Motiv, Komposition und Bewegung bei, während generate_clip_video neu aus dem Storyboard-Bild generiert. enhance_clip_image passt Gesichter wieder an die Steckbriefe des Casts an. regenerate_clip führt Bild- und dann Video-Generierung als einen Job aus – die richtige Wahl nach der Änderung der Beschreibung oder des Casts eines Clips. generate_clip_grid_reference zeichnet ein Kontinuitätsblatt, dessen Panels aus den eigenen Zeitpunkten des Clips stammen, nicht aus einem festen Satz generischer Momente. |
| 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. |
Ausgaben sind doppelt gesichert Ein ★-Tool verursacht beim ersten Aufruf nie Kosten: Es gibt ein Credit-Angebot und ein signiertes confirmToken zurück, und der Agent muss dir das Angebot zeigen und darf nur mit deiner ausdrücklichen Zustimmung fortfahren. Unabhängig davon kann ein Key ohne stories:generate physisch keine Kosten verursachen – erstelle einen Key nur für Bearbeitungen mit stories:write, wenn du einen Agenten zum Lesen und Editieren möchtest.
Wie es sicher und korrekt bleibt
- Der Server erzwingt die Regeln des Editors (Verschieben von Slides, Trim-Grenzen, Untertitel-Spuren, Overlay-Zeilen) und beantwortet einen Verstoß mit dem erlaubten Bereich, anstatt ihn stillschweigend zu kappen.
- Der Agent liest das vollständige Regelwerk aus der Ressource yoh://guide/timeline, und der Prompt /mcp__yoh__edit-story gibt ihm einen sicheren Workflow vor.
- Die Generierung erfolgt asynchron – die meisten Tools geben eine runId zurück, die der Agent mit get_run abfragt; einige wenige (create_story, render_story, Charakter-Grafik) geben eine jobId für get_job zurück, genau wie die REST-API.
- Setze einen hohen Timeout in der .mcp.json (zum Beispiel 900000), damit ein Rendern auf Client-Seite nicht abbricht.
Verbindung verifizieren
Ein Smoke-Test wird mit dem Repo ausgeliefert. Er nutzt das echte Protokoll, prüft die Regeln und beweist, dass die Ausgabensperre hält – er verbraucht niemals Credits, und sein einziger Schreibvorgang wird rückgängig gemacht:
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-writeAPI-Keys verwalten
Endpunkte für die API-Key-Verwaltung verwenden session authentication (Cookie des angemeldeten Nutzers), keinen API-Key. Nutze das API Keys page für einen browserbasierten Workflow oder rufe diese Endpunkte programmatisch von deinem Backend mit einer gültigen Session auf. /api-keys
API-Keys auflisten
/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"
}]
}Einen API-Key erstellen
/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"]
}Einen API-Key widerrufen
/api/v1/api-keys/:idscope: session{ "id": "key_abc123", "revoked": true }Fehler
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.
Die meisten Endpunkte geben ein error-Objekt mit einem maschinenlesbaren code zurück. Spezialisierte Endpunkte können einen Fehler als String oder einen Top-Level-Code zurückgeben; prüfe immer den HTTP-Status.
{ "error": { "code": "insufficient_credits", "message": "...", "status": 402 } }| HTTP | Code | Bedeutung |
|---|---|---|
| 401 | unauthorized | Fehlender oder ungültiger API-Key. |
| 403 | forbidden | API-Key hat nicht die erforderliche Berechtigung. |
| 404 | not_found | Die angeforderte Ressource existiert nicht. |
| 400 | bad_request | Ungültiger Request-Body oder Parameter. |
| 402 | insufficient_credits | Nicht genügend Credits, um den Vorgang auszuführen. |
| 429 | rate_limited | Zu viele Anfragen. Prüfe den Retry-After-Header. |
| 500 | internal_error | Unerwarteter Serverfehler. |
Rate-Limits: Endpunkte zum Generieren erlauben 5 Anfragen pro Minute pro API-Key. Lesezugriffe und Clip-Bearbeitungen (die keine Credits verbrauchen) erlauben 60. Wenn du ein Limit erreichst, enthält die Antwort einen Retry-After-Header mit der Anzahl der Sekunden, die du warten musst. Fehlgeschlagene Jobs (nach allen Versuchen) erstatten die abgezogenen Credits automatisch zurück.
Auf Social Media veröffentlichen
Übertrage ein fertiges Video direkt an einen verknüpften Account, sodass eine Automatisierung generieren → rendern → veröffentlichen mit nur einem Zugang steuern kann. Verknüpfe die Accounts zuerst in der App; dieser Endpunkt veröffentlicht auf ihnen.
Ein Video veröffentlichen
/api/v1/social/publishscope: social:publishGibt 202 mit { postId, status: "publishing" } zurück und lädt im Hintergrund hoch – frage dazu den Status-Endpunkt unten ab. Übergib wait: true, um stattdessen zu blockieren, bis der Upload fertig ist (das kann Minuten dauern). Die Video-URL muss zu einem erfolgreichen Rendern oder zum ursprünglich hochgeladenen Video dieser Story gehören. YouTube-Streams sind auf öffentliche Adressen und das bestehende Upload-Limit von 512 MB beschränkt.
socialAccountIdstringstoryIdstringvideoUrlstringplatformstringcaptionstringyoutubeobjectdurationSecnumberaspectRatiostringfileSizeBytesnumberwaitbooleanVeröffentlichungsstatus abfragen
/api/v1/social/publish/:postIdscope: social:publishLebenszyklus:
publishing→published|failed.