API-referentie
Genereer AI-video's via code. Dien een prompt in, poll tot de taak klaar is en ontvang een directe video-URL, allemaal via REST.
Base URL: https://www.yoh.appCodevoorbeelden gebruiken de tijdelijke aanduiding sk_live_YOUR_API_KEY totdat je inlogt.
Snelstart
Maak je eerste video in drie API-aanroepen: genereren → pollen → downloaden.
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
}Dat is alles. De videoUrl is een publiek toegankelijke URL die je direct kunt downloaden, streamen of delen.
Authenticatie
Elk API-verzoek moet je API-key bevatten in de Authorization-header, met het Bearer-schema.
Authorization: Bearer sk_live_YOUR_API_KEYAPI-keys beginnen met sk_live_. Je kunt keys aanmaken en beheren op de API Keys page. /api-keys
Houd je API-keys geheim. Zet ze niet in client-side code of openbare repository's. Is een key uitgelekt, trek hem dan meteen in op de API-keys-pagina.
Scopes & Credits
Elke API-key krijgt een of meer scopes die bepalen tot welke endpoints hij toegang heeft. Tekst voor een verhaal genereren kost 55 credits per taak; scopes die ook clipvideo's of een eindrender maken (waaronder de standaardscope, full), rekenen die vooraf extra af. Mislukt de taak na alle nieuwe pogingen, dan krijg je de credits terug.
| Scope | Geeft toegang tot |
|---|---|
| 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. |
Een video genereren
Een videogeneratietaak in de wachtrij zetten
/api/v1/stories/generatescope: stories:generateDient een prompt in en geeft direct een jobId terug. De video wordt asynchroon gegenereerd. Poll GET /api/v1/stories/jobs/:jobId om de voortgang te volgen.
Request body
| Veld | Type | Status | Beschrijving |
|---|---|---|---|
prompt | string | verplicht | Beschrijving van het verhaal, max. 5000 tekens. |
aspectRatio | string | optioneel | Een van 16:9, 9:16, 1:1, 21:9. Standaard: 16:9. |
renderVideo | boolean | optioneel | false: geeft de eerste AI-clip terug (snel, ~10 min). true: rendert alle clips via Remotion tot één samengestelde MP4 (~12–15 min). |
scope | string | optioneel | 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 | optioneel | Weergavenaam van het verhaal. |
contentType | string | optioneel | Formaatcategorie van het verhaal. Een van cinematic_story, news_analysis, persona_channel, ad_spot. Standaard: cinematic_story. |
locale | string | optioneel | Taalcode, bijv. en, zh-TW. Standaard: en. |
seriesId | string | optioneel | Koppel aan een bestaand verhaal voor een consistent uiterlijk van de personages. |
assets | array | optioneel | 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 | optioneel | 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 | optioneel | Implied by continuityMode: "finale"; you rarely need both. |
allowMultiStory | boolean | optioneel | 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 | optioneel | 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[] optioneel
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.
| Veld | Type | Beschrijving |
|---|---|---|
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 optioneel
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.
| Veld | Type | Waarden | Standaard |
|---|---|---|---|
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 optioneel
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 | Velden |
|---|---|
| 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 optionele velden
| Veld | Type | Beschrijving |
|---|---|---|
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"
}
}'Antwoord 202 Accepted
{ "jobId": "cmnk8wu2q0001q3vpmqv93nmy", "status": "queued", "pollUrl": "/api/v1/stories/jobs/..." }Stapsgewijze endpoints
After generating a story with scope: "story", use these endpoints to generate storyboard images, clip videos, and the final rendered MP4 as separate steps.
/api/v1/stories/:storyId/assetsGenerate the cast and world: character portraits + 2×2 character sheets, plus scene and item images. Run this before /storyboard for the best face consistency. Skips assets that already have images. 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.
Eén clip tegelijk
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 replacedHet verhaal bewerken
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 → videoUrlStatus van taak opvragen
Taakstatus ophalen
/api/v1/stories/jobs/:jobIdscope: stories:readPoll dit endpoint nadat je POST /api/v1/stories/generate hebt aangeroepen. Een taak duurt meestal 8–15 minuten, afhankelijk van de lengte van het verhaal en of renderVideo aanstaat. We raden aan om elke 15–30 seconden te pollen.
Levenscyclus van taakstatus
queuedgenerating_storygenerating_assetsgenerating_videosrendering_videocompletedoffailedWhich 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.
Velden in het antwoord
| Veld | Type | Beschrijving |
|---|---|---|
jobId | string | Unieke taak-ID. |
status | string | Huidige status. Zie de levenscyclus hierboven. |
progress | number | Geheel getal van 0–100 dat het voltooiingspercentage aangeeft. |
storyId | string|null | Wordt ingevuld zodra het verhaal is gegenereerd. Gebruik dit om assets op te halen. |
renderVideo | boolean | Of er een samengestelde definitieve MP4 is aangevraagd. |
videoUrl | string|null | Absolute video-URL wanneer de status completed is. Ofwel een samengestelde MP4 (renderVideo: true), ofwel de AI-video van de eerste clip (renderVideo: false). |
error | string|null | Leesbare foutmelding. Wordt ingevuld bij een fout, en ook terwijl een taak opnieuw wordt geprobeerd (de status gaat dan terug naar queued). Altijd null bij succes. |
waitingFor / message | string (optional) | Aanwezig zolang een taak op een resultaat van een provider wacht (status blijft queued, error is null): de waarde is “provider” en message zegt waarop wordt gewacht. Blijf pollen. |
execution | object (optional) | De Workflow-run die eigenaar is van de taak: { version, runId, phase, heartbeatAt, status }. Volg deze via GET /api/runs/:runId (of de /stream ervan). Ontbreekt bij oudere taken. |
createdAt / startedAt / completedAt | string (ISO) | Tijdstempels. |
runId / runUrl | string (202 only) | Bij het 202-antwoord van elke taakstart (niet bij deze poll): de run die voor de taak is gestart en de bijbehorende status-URL. Optioneel te gebruiken; het pollen van dit eindpunt is voldoende. |
Verhalen
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.
Verhalen ophalen
/api/v1/storiesscope: stories:readGeeft een gepagineerde lijst van je verhalen terug.
Query-parameters
| Parameter | Standaard | Beschrijving |
|---|---|---|
page | 1 | Paginanummer (≥ 1). |
limit | 20 | Resultaten per pagina (1–100). Capped at 100. |
status | — | Filteren op status van het verhaal. |
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 }
}Een verhaal ophalen
/api/v1/stories/:storyIdscope: stories:readGeeft de volledige metadata van één verhaal terug, inclusief het aantal personages, scènes, items en 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"
}Overzicht van assets
Als een taak klaar is, kun je alle assets die voor het verhaal zijn gegenereerd ophalen met de storyId die de taak teruggeeft. Alle asset-endpoints vereisen de scope assets:read.
GET /api/v1/stories/:id/charactersPersonages — naam, beschrijving, rol, imageUrl, steminformatie
GET /api/v1/stories/:id/scenesScènes — beschrijving, tijdstip, weer, belichting, imageUrl
GET /api/v1/stories/:id/itemsRekwisieten / items — beschrijving, betekenis, imageUrl
GET /api/v1/stories/:id/clipsClips — vertelling, duur, videoUrl, imageUrl, audioUrl
Personages
Personages ophalen
/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 & Video's
Clips ophalen
/api/v1/stories/:storyId/clipsscope: assets:readElke clip is een scènesegment met een eigen door AI gegenereerde video, stilstaand beeld en audio. Alle URL-velden zijn absoluut en openbaar toegankelijk.
{
"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"
}]
}Wanneer renderVideo false is, verwijst de videoUrl van de taak naar de video van de eerste clip. Gebruik dit eindpunt om de video-URL voor elke afzonderlijke clip op te halen.
Asset-bibliotheek van het verhaal
Upload een asset één keer en verwijs er daarna altijd naar
/api/v1/series/:seriesId/assetsscope: assets:writeDezelfde bibliotheek waar het assetpaneel in de studio naar schrijft. Upload een personage, achtergrond, item of stijlreferentie, bewaar de id die je terugkrijgt en verwijs er bij het genereren naar door die id als seriesAssetId in de assets-array mee te geven. Je hoeft dus niet per video een nieuwe afbeeldingslink te maken. Afbeeldingen die je meestuurt, worden opnieuw gehost in onze opslag, zodat de id blijft werken nadat je eigen upload-URL is verlopen.
| Veld | Type | Beschrijving |
|---|---|---|
type | string | personage, scène, item, of referentie. |
name | string | Weergavenaam. Ook de sleutel waar latere generaties mee matchen. |
imageUrl | string | Publieke URL van de afbeelding. Stuur anders imageBase64. |
itemType | string | Alleen items: prop, product, of logo. Een logo wordt plat geplaatst en krijgt nooit 3D-diepte. |
isHeroProduct | boolean | Alleen items: het product waar deze advertentie over gaat. Het verschijnt in elke clip. |
isPrimaryBackground | boolean | Alleen scènes: de studio/set die in elk ankershot terugkomt. |
seriesDuplicate | string | Alleen personages, na overleg met de gebruiker: "new" voegt een tweede personage toe met een naam die al in het verhaal voorkomt. |
- Stuur één asset, of een "assets"-array om er tot 100 tegelijk te upserten.
- Standaard worden assets samengevoegd bij een exact gelijke naam, dus dezelfde catalogus opnieuw uploaden is idempotent. Behalve bij een personage waarvan de naam al in het verhaal voorkomt: dat geeft 409 SERIES_NAME_EXISTS met het bestaande personage, en er wordt niets opgeslagen. Vraag het de gebruiker en stuur dan seriesDuplicate "new" om een tweede toe te voegen, of geef ?mergeStrategy=exact mee om het bestaande bij te werken. ?mergeStrategy=fuzzy kan tegen hernoemingen; insert maakt altijd een nieuwe asset aan.
- DELETE geeft 409 SERIES_ASSET_IN_USE met de afleveringen die de asset nog gebruiken (stories: id, title, clipCount) en verwijdert niets. Vraag het de gebruiker en stuur dan confirm: true (body of ?confirm=true): afleveringen houden hun eigen kopie en hun clips veranderen niet. Een personage met een lopende inhuring geeft 409 SERIES_ASSET_HIRED (hires) en kan pas worden verwijderd als de inhuring is afgelopen.
- Stel isHeroProduct in op een item (ad_spot) of isPrimaryBackground op een scène (news_analysis / persona_channel) om het vast te pinnen. Bij het genereren wordt de pin automatisch herkend — je hoeft heroItemId nooit handmatig te schrijven.
| Scope | Geeft toegang tot |
|---|---|
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.
Verhaal-assets
Een personageafbeelding registreren voor een verhaal
/api/v1/series-assets/ensure-character-imagescope: assets:writePOST /api/v1/series-assets/ensure-character-image werkt nog steeds en is ongewijzigd, maar ondersteunt alleen personages. Gebruik bij voorkeur dit endpoint.
Uploadt en bewaart een referentieafbeelding van een personage voor een verhaal, zodat de AI een consistent gezicht en een consistente stijl gebruikt in alle verhalen. Deze bewerking is idempotent — het meerdere keren aanroepen met dezelfde personagenaam is veilig.
| Veld | Type | Beschrijving |
|---|---|---|
seriesId | string | ID van het verhaal waaraan het personage moet worden gekoppeld. |
characterName | string | De naam van het personage (gebruikt als unieke sleutel binnen het verhaal). |
imageUrl | string | Openbaar toegankelijke URL van de referentieafbeelding van het personage. |
Beheerde accounts
Voor platforms met veel klanten. In plaats van één key en één gedeelde creditpot voor iedereen, waarbij één drukke klant het saldo voor de rest opmaakt en niemand kan zien wie wat heeft uitgegeven, maak je per klant een account aan en vul je dat aan vanuit je eigen saldo. Je klanten registreren zich hier nooit en betalen ons niet; de commerciële relatie blijft van jou.
Een account aanmaken
/api/v1/accountsscope: accounts:manageexternalId is je eigen id voor de klant. Twee keer aanroepen met dezelfde id geeft het bestaande account terug in plaats van een tweede aan te maken, dus een herhaald verzoek is veilig. De API-key krijg je één keer, bij het aanmaken, terug: we bewaren alleen een hash en kunnen hem niet opnieuw tonen, dus sla hem meteen op. Een nieuw account begint met een saldo van nul.
| Veld | Type | Beschrijving |
|---|---|---|
externalId | string | Verplicht. Je eigen id voor deze klant, bijvoorbeeld een tenant-id. Uniek per platform. |
name | string | Optioneel label, voor je eigen rapportages. |
Key van een beheerd account vervangen
/api/v1/accounts/:accountId/api-keysscope: accounts:manageIs de oorspronkelijke key kwijt, roep dit endpoint dan aan met de key van het platform dat eigenaar is, zonder request body. Gebruik de account-ID of externalId. Dit trekt de vorige keys van het account in één keer in en geeft eenmalig een nieuwe apiKey terug (201). Bewaar die veilig. Herhaal je deze aanroep, dan wordt de key opnieuw vervangen; gewone nieuwe pogingen om een account aan te maken vervangen nooit een werkende key.
Een account opwaarderen
/api/v1/accounts/:accountId/creditsscope: accounts:manageZet credits over van je eigen saldo naar dat van het account. Het is een overboeking, geen gift, dus je kunt alleen weggeven wat je echt hebt gekocht. Geeft 402 als je eigen saldo niet toereikend is. :accountId accepteert zowel onze account-id als je externalId.
| Veld | Type | Beschrijving |
|---|---|---|
amount | integer | Verplicht. Aantal credits om over te zetten. Moet positief zijn. |
idempotencyKey | string | Geef je factuur-id of abonnementsperiode-id mee. Een herhaalde of opnieuw afgeleverde aanroep geeft dan de oorspronkelijke overboeking terug in plaats van het account twee keer op te waarderen. Probeer opnieuw met dezelfde key, ontvanger en hetzelfde bedrag. Een herhaling zet geen credits opnieuw over, ook niet als het platformsaldo inmiddels op is. Een andere ontvanger of een ander bedrag geeft 409. Te lange keys worden geweigerd in plaats van ingekort. |
description | string | Optionele notitie, zichtbaar aan beide kanten van de overboeking in de creditgeschiedenis. |
GET /api/v1/accounts toont je accounts met het saldo en de totale uitgaven van elk; GET /api/v1/accounts/:accountId/credits leest er één. De eigen key van een beheerd account heeft alleen generatiescopes: daarmee kun je nooit zelf accounts aanmaken of opwaarderen.
Claude Code & Codex (MCP)
Yoh heeft een MCP-server, zodat AI-agents (Claude Code, Codex, Claude Desktop of elke andere MCP-client) verhalen in je account kunnen maken en bewerken: een nieuw verhaal schrijven vanuit een idee, de personages toevoegen en de video genereren, of de inhoud en dialoog van clips, verplaatsingen en inkortingen op de tijdlijn en vrije overlays wijzigen. Alles wat credits kost, wacht elke keer op je uitdrukkelijke goedkeuring. Het volgt dezelfde regels als de editor in de browser, dus een bewerking vanuit de terminal komt precies terecht waar een sleepactie terecht zou komen.
Drie manieren om het te gebruiken
Kies de manier die past bij de tools van je agent. De yoh-skill hieronder leert ze alle drie.
- MCP + browser (aanbevolen in Codex, Claude Code en de Claude-apps) De agent doet het werk via deze MCP-tools en bestuurt daarnaast een browsertabblad (Claude in Chrome, de browser van Codex): na elke stap opent hij de link uit het antwoord (de scripttabel na een nieuw verhaal, het clippaneel na een clipvideo, de lijst bij Alles genereren terwijl een run loopt, de geavanceerde editor voor de tijdlijn en tekstlagen, de MP4 van de render) en controleert hij het resultaat op de echte pagina. Log in die browser één keer in met hetzelfde account als de API-key. MCP en de pagina delen dezelfde gegevens: de afleveringspagina ververst zichzelf binnen enkele seconden na een MCP-wijziging, en de agent herlaadt de geavanceerde editor na een wijziging op de tijdlijn. Elke uitgave loopt via één weg: de MCP-prijsopgave of het prijsvenster van de app.
- Alleen browser (geen API-key) De agent werkt in je browser op de pagina's van yoh.app, zoals jij dat zou doen: Maken → Verhaal, de scripttabel en het clippaneel, de personage-editor en de modelkiezer, Alles genereren, de geavanceerde editor, versies en export. Elke uitgave loopt via het eigen prijsvenster van de app, en de agent typt nooit betaalgegevens of je inlogcode in.
- Alleen MCP (headless) Voor scripts, CI en bureaus die veel varianten maken: quote → confirmToken voor elke uitgave, poll get_job / get_run, stop met cancel_generation, maak varianten met create_story_versions en generate_all_clips, en download de videoUrl van elke voltooide render. Geen browser nodig.
Voorbeeld in elke modus: een advertentie van 16 seconden in 9:16, in het Engels en Japans, geschreven in een verhaal met het product, gegenereerd, als Japanse versie gemaakt en twee keer gerenderd.
Verbinden
Aanbevolen: log in met Yoh, geen API-sleutel nodig. Gebruik precies dit adres (yoh.app zonder www stuurt door, en het inloggen controleert het adres). In Claude Code:
claude mcp add --transport http --scope user yoh https://www.yoh.app/api/mcpTyp daarna /mcp, kies yoh en dan Authenticate, log in bij Yoh in de browser die opent met het account dat je wilt gebruiken, en keur goed.
In Codex (CLI, IDE-extensie of de ChatGPT-desktopapp) voeg je de server toe en log je in; zet daarna tool_timeout_sec = 300 onder [mcp_servers.yoh] in ~/.codex/config.toml en herstart Codex:
codex mcp add yoh --url https://www.yoh.app/api/mcp
codex mcp login yoh- Claude-desktopapp of claude.ai: Customize → Connectors → + Add → Add custom connector, noem hem Yoh, plak het adres, dan Connect en inloggen. Zet hem per chat aan via + → Connectors. Bij Team- of Enterprise-abonnementen voegt een eigenaar hem eerst toe.
- Cursor: voeg yoh met dit adres toe aan ~/.cursor/mcp.json en log in als Cursor erom vraagt.
- ChatGPT: Plugins → + → Add custom MCP server, noem hem Yoh, plak het adres en log in. Bij sommige abonnementen moet een beheerder eigen servers eerst toestaan.
Controleer het: vraag je agent get_credits uit te voeren; die hoort je Yoh-saldo te tonen. Gekoppelde apps zie en ontkoppel je via Instellingen → API-sleutels → Gekoppelde apps.
Zodat je agent de pagina’s ziet die hij wijzigt: Claude Code met claude --chrome (de extensie Claude in Chrome), de Claude-desktopapp met de connector Claude in Chrome, of Codex in de ChatGPT-desktopapp met zijn browser. De skill vertelt de agent wanneer hij moet kijken.
Of gebruik een API-sleutel (scripts, CI, apps zonder inloggen)
Maak een API-key aan op /api-keys. Wil je nieuwe verhalen maken en video genereren, voeg dan stories:generate toe; om te bewerken zonder credits uit te geven, zijn stories:read en stories:write genoeg. Daarna in Claude Code:
claude mcp add --transport http yoh https://www.yoh.app/api/mcp \
--header "Authorization: Bearer sk_live_YOUR_API_KEY"Of commit een projectconfiguratie, zodat een heel team de verbinding krijgt bij het openen van de repo; ieder lid levert zijn eigen key via env:
{
"mcpServers": {
"yoh": {
"type": "http",
"url": "https://www.yoh.app/api/mcp",
"headers": { "Authorization": "Bearer ${YOH_API_KEY}" },
"timeout": 900000
}
}
}Voeg in Codex de server toe aan ~/.codex/config.toml en stel YOH_API_KEY in je shell in:
# 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 = 300Installeer de Yoh-skill
De skill leert je agent hoe hij een Yoh-verhaal goed maakt en bewerkt: begin in een verhaal, bepaal de prijs voordat je uitgeeft en bekijk de clips voordat je ze beoordeelt. Het is één bestand dat gelijk blijft met de server. Voer de regel voor je agent uit, of download 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.mdVraag dan gewoon: “maak een verhaal van 60 seconden over een nachtverpleegkundige die geesten kan horen” of “maak het tempo van aflevering 3 strakker: haal de stiltes eruit, zet de ondertitels op de beats en render opnieuw”. Voer /mcp uit in Claude Code of Codex om de verbinding te controleren.
Wat de agent kan doen
| Tools | Scope | Voorbeelden |
|---|---|---|
| Lezen | 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 voegt de storyboardafbeelding toe en, met withFrames, echte frames uit de gegenereerde video: de enige manier waarop de agent kan zien hoe een clip er echt uitziet. get_asset_images doet hetzelfde voor cast, sets en rekwisieten. |
| 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. |
| Maken ★ | stories:generate | create_storySchrijf een gloednieuw verhaal vanuit een prompt. estimate_story is gratis en berekent vooraf de prijs van een heel plan, wat de prijsopgaven per actie niet kunnen. Begin met scope="story" (alleen tekst) in plaats van "full", dat direct video in rekening brengt. |
| 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. |
| Verhalen | 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_mainDe laag boven één verhaal: de gedeelde personages, achtergronden en stijl die over afleveringen heen doorlopen. Koppel een nieuw verhaal met create_story seriesId om ze over te nemen. |
| Clips & dialoog | 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_settingsDialoog die niet meer in zijn clip past, verlengt de clip automatisch. Tekstwijzigingen veranderen nooit al gegenereerde media. update_clips bewerkt veel clips tegelijk, elk met eigen waarden; set_all_clips zet één waarde op elke clip en weigert tekst, omdat overal dezelfde beschrijving de aflevering eentonig maakt. Beide wijzen de hele batch af als er één item fout is. reorder_clips stelt in één aanroep de hele volgorde in. |
| Yoh-assistent | stories:write | ask_assistant · apply_assistant · undo_assistantGeef een verzoek door aan Yohs eigen assistent, die achter Vraag AI op de scriptpagina en de AI-agent in de editor zit. ask_assistant doet alleen voorstellen: bewerkingen komen terug als wijzigingen en betaalde stappen als prijsopgaven. apply_assistant voert de wijzigingen uit die je houdt; voor een betaalde stap is ook de confirmToken van de prijsopgave nodig nadat de gebruiker akkoord is. undo_assistant draait terug wat een toegepaste run heeft gewijzigd. Elke ask_assistant-beurt kost een paar credits: de werkelijke AI-kosten, tot een kleine prijsopgave per beurt. |
| Tijdlijn | 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_soundtrackEen clip verplaatsen schuift alle latere clips mee; bij het aanpassen van de lengte blijft het begin vast, dus het begin verslepen is een trim aan de voorkant. Verhaalitems kunnen niet worden verwijderd of gesplitst: verberg ze, of exporteer ze naar een vrije overlay. |
| Overlays | stories:write | add_overlay · update_overlay · move_overlay · resize_overlay · split_overlay · duplicate_overlay · delete_overlay · delete_freeform_row · upload_mediaOverlays mogen elkaar niet overlappen op een rij; een conflicterende plaatsing wordt geweigerd met een lijst van vrije rijen. Media vereisen een gehoste https-bron. |
| Stijl | stories:write | apply_overlay_preset · set_caption_styleDe eigen tekstpresets, ondertitelingstemplates en ~32 in- en uitgangsanimaties van de editor, zodat de agent toepast wat het product biedt in plaats van zelf CSS te verzinnen. De opmaak van ondertitels geldt per dialoogregel. |
| Personages & sets | stories:write | add_character · update_character · delete_character · add_scene · update_scene · delete_scene · add_item · update_item · delete_item · keep_character_sheetEen personage verwijderen haalt het uit elke clip. Zijn dialoogregels blijven bewaard zonder spreker tot je kiest wie ze uitspreekt (update_dialogue_line); een rol met een gelicentieerd gezicht of een openstaande inhuring kan niet worden verwijderd. |
| Stemcasting | stories:write | set_character_voiceStandaardstemmen, gefilterd op de taal van het verhaal, plus de gekloonde stemmen van het account die klaar zijn. Casting is gratis; dialoog die al is ingesproken, houdt de stem waarmee hij is gerenderd. |
| Opnieuw inspreken | stories:write | set_revoice_enabled · select_revoice_take · undo_revoice · fix_audio_syncSchakelen tussen de nieuwe opnames en de originele uitvoering is gratis en omkeerbaar; de takes blijven in beide gevallen bewaard. |
| Genereren ★ | 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_storyElk paar is bewerken versus opnieuw tekenen: edit_clip_image herschrijft het huidige frame op basis van een instructie, terwijl regenerate_clip_image een nieuw frame tekent; edit_clip_video is video-naar-video en behoudt onderwerp, compositie en beweging, terwijl generate_clip_video opnieuw genereert vanaf de storyboardafbeelding. enhance_clip_image laat gezichten weer lijken op de personagekaarten. regenerate_clip doet eerst de afbeelding en dan de video in één taak: de juiste keuze nadat je de beschrijving of cast van een clip hebt gewijzigd. generate_clip_grid_reference tekent een continuïteitsblad met panelen uit de eigen tijdsmomenten van de clip, niet uit een vaste set algemene momenten. |
| 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. |
Uitgaven zijn dubbel beveiligd Een ★-tool geeft bij de eerste aanroep nooit iets uit: hij geeft een prijsopgave en een ondertekende confirmToken terug, en de agent moet je de prijsopgave laten zien en mag pas doorgaan met jouw uitdrukkelijke goedkeuring. Daarnaast kan een key zonder stories:generate gewoon niets uitgeven; maak een key met alleen stories:write als je een agent wilt die alleen leest en bewerkt.
Hoe het veilig en correct blijft
- De server handhaaft de regels van de editor (verschuivingen, grenzen voor inkorten, ondertitelsporen, overlay-rijen) en beantwoordt een overtreding met het toegestane bereik in plaats van het stilletjes te corrigeren.
- De agent leest alle regels uit de bron yoh://guide/timeline, en de prompt /mcp__yoh__edit-story geeft hem een veilige werkwijze.
- Genereren is asynchroon: de meeste tools geven een runId terug die de agent met get_run pollt; een paar (create_story, render_story, personageafbeeldingen) geven een jobId voor get_job terug, net als de REST API.
- Stel in .mcp.json een lange timeout in (bijvoorbeeld 900000), zodat een render aan de clientkant niet verloopt.
Controleer je verbinding
De repo bevat een smoketest. Die spreekt het echte protocol, test de regels en bewijst dat de uitgavebeveiliging werkt. Hij geeft nooit credits uit en zijn enige schrijfactie wordt teruggedraaid:
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 beheren
Endpoints voor het beheer van API-keys gebruiken session authentication (cookie van de ingelogde gebruiker), geen API-key. Gebruik de API Keys page voor een workflow via de browser, of roep deze endpoints programmatisch aan vanuit je backend met een geldige sessie. /api-keys
API-keys ophalen
/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"
}]
}Een API-key aanmaken
/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"]
}Een API-key intrekken
/api/v1/api-keys/:idscope: session{ "id": "key_abc123", "revoked": true }Fouten
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.
De meeste endpoints retourneren een error-object met een machineleesbare code. Gespecialiseerde endpoints kunnen een foutmelding als tekst of een code op het hoogste niveau retourneren; controleer altijd de HTTP-status.
{ "error": { "code": "insufficient_credits", "message": "...", "status": 402 } }| HTTP | code | Betekenis |
|---|---|---|
| 401 | unauthorized | Ontbrekende of ongeldige API-key. |
| 403 | forbidden | API-key heeft niet de vereiste scope. |
| 404 | not_found | De opgevraagde bron bestaat niet. |
| 400 | bad_request | Ongeldige request body of parameters. |
| 402 | insufficient_credits | Niet genoeg credits om de bewerking uit te voeren. |
| 429 | rate_limited | Te veel verzoeken. Controleer de Retry-After-header. |
| 500 | internal_error | Onverwachte serverfout. |
Snelheidslimieten: Generatie-endpoints staan 5 verzoeken per minuut per API-key toe. Lezen en clipbewerkingen (die geen credits kosten) staan er 60 toe. Bereik je een limiet, dan bevat het antwoord een Retry-After-header met het aantal seconden dat je moet wachten. Mislukte taken (na alle nieuwe pogingen) krijgen de afgeschreven credits automatisch terug.
Publiceren op sociale media
Stuur een voltooide video rechtstreeks naar een gekoppeld account, zodat een automatisering genereren → renderen → publiceren met één set inloggegevens kan aansturen. Koppel de accounts eerst in de app; dit endpoint publiceert ernaar.
Een video publiceren
/api/v1/social/publishscope: social:publishGeeft 202 terug met { postId, status: "publishing" } en uploadt op de achtergrond; poll het status-endpoint hieronder. Geef wait: true mee om in plaats daarvan te wachten tot de upload klaar is (dat kan minuten duren). De video-URL moet overeenkomen met een geslaagde render of de oorspronkelijk geüploade video van dit verhaal. YouTube-streams zijn beperkt tot openbare adressen en de bestaande uploadlimiet van 512 MB.
socialAccountIdstringstoryIdstringvideoUrlstringplatformstringcaptionstringyoutubeobjectdurationSecnumberaspectRatiostringfileSizeBytesnumberwaitbooleanPublicatiestatus pollen
/api/v1/social/publish/:postIdscope: social:publishLevenscyclus:
publishing→published|failed.