Referencia de la API

Referencia de la API

Genera vídeos con IA mediante programación. Envía un prompt, consulta el estado y recibe la URL directa del vídeo, todo vía REST.

URL base: https://www.yoh.app

Los ejemplos de código usarán el marcador sk_live_YOUR_API_KEY hasta que inicies sesión.

Inicio rápido

Crea tu primer vídeo en tres llamadas a la API: generar → consultar → descargar.

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.

Comprueba tu clave y saldo (sin coste)
curl https://www.yoh.app/api/v1/credits \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"

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

1. Generar un vídeo
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.

Respuesta — tarea en cola
{
  "jobId": "cmnk8wu2q0001q3vpmqv93nmy",
  "status": "queued",
  "pollUrl": "/api/v1/stories/jobs/cmnk8wu2q0001q3vpmqv93nmy"
}
2. Consultar hasta completar
curl https://www.yoh.app/api/v1/stories/jobs/cmnk8wu2q0001q3vpmqv93nmy \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
Respuesta — tarea completada
{
  "status": "completed", "progress": 100,
  "storyId": "cmnk8xxnx000004lbd7siief7",
  "videoUrl": "https://cdn.example.com/video/clip.mp4",
  "error": null
}

Eso es todo. La videoUrl es una URL de acceso público que puedes descargar, reproducir en streaming o compartir de inmediato.

Autenticación

Todas las solicitudes de la API deben incluir tu clave de API en la cabecera Authorization utilizando el esquema Bearer.

Authorization: Bearer sk_live_YOUR_API_KEY

Las claves de API comienzan por sk_live_. Puedes crear y gestionar tus claves desde la API Keys page. /api-keys

Mantén tus claves de API en secreto. No las expongas en código del lado del cliente ni en repositorios públicos. Si una clave se ve comprometida, revócala de inmediato desde la página de claves de API.

Permisos y créditos

Cada clave de API tiene uno o más permisos que controlan a qué endpoints puede acceder. Generar el texto de la historia cuesta 55 créditos por tarea; los permisos que además producen vídeos de clips o un renderizado final (incluido el permiso predeterminado, full) cobran eso aparte y por adelantado. Los créditos se reembolsan si la tarea falla tras todos los reintentos.

PermisoDa acceso a
stories:generatePOST /api/v1/stories/generate, POST /stories/:id/assets, POST /stories/:id/storyboard, POST /stories/:id/videos, POST /stories/:id/render, POST /stories/:id/clips/:clipId/image, POST /stories/:id/clips/:clipId/video, POST /stories/:id/clips/:clipId/video-edit, POST /stories/:id/characters/:assetId/sheet, POST /stories/:id/soundtrack, POST /stories/:id/assets/:assetId/image, POST /stories/:id/clips/:clipId/regenerate, POST /stories/:id/continue, POST /stories/:id/clips-all, POST /stories/:id/clips/:clipId/grid-reference — and the story-asset edits: PATCH /stories/:id/characters|scenes|items/:assetId. Also implies assets:write and series:write, so keys issued before those scopes existed keep working.
stories:readGET /api/v1/stories, GET /api/v1/stories/:id, GET /api/v1/stories/jobs/:id, GET /api/v1/credits, GET /api/v1/models (the models and visual styles the pickers offer, with durations, resolutions, aspect ratios, reference limits, price hints and retired ids), POST /api/v1/stories/estimate, POST /api/v1/stories/analyze-prompt. Implies series:read and assets:read.
stories:writeThe clip edits: POST/PATCH /stories/:id/clips, PATCH/DELETE /stories/:id/clips/:clipId, POST /stories/:id/clips/:clipId/duplicate; a hired character's looks: POST /stories/:id/characters/:assetId/hire-looks; starting an episode from your own video: POST /api/v1/series/:id/uploads, POST /stories/:id/upload (GET with stories:read; so is GET /stories/:id/media, every file of a story); and the MCP server's editing tools (POST /api/mcp): clip content and dialogue, timeline moves/trims/captions, freeform overlays, cast and set edits. Spends no credits, so a key with this scope but WITHOUT stories:generate can edit a story and physically cannot pay to generate anything. Implied by stories:generate; implies assets:write.
assets:readGET .../characters, .../scenes, .../items, .../clips, GET .../characters/:assetId/hire-looks, GET /api/v1/series/:id/assets
assets:writePOST /api/v1/series/:id/assets, PATCH/DELETE /api/v1/series/:id/assets/:assetId, POST /api/v1/uploads, POST /api/v1/uploads/presign (a one-time PUT URL for an audio, video or image file)
series:readGET /api/v1/series, GET /api/v1/series/:id
series:writePOST /api/v1/series, PATCH /api/v1/series/:id
social:publishPOST /api/v1/social/publish, GET /api/v1/social/publish/:postId
accounts:manageGET/POST /api/v1/accounts, GET/POST /api/v1/accounts/:accountId/credits. For platforms that serve many customers: provision an account per customer and fund it from your own balance. Implied by nothing — an existing key never gains it.

Generar un vídeo

Poner en cola una tarea de generación de vídeo

POST/api/v1/stories/generatescope: stories:generate

Envía un prompt y devuelve un jobId de inmediato. El vídeo se genera de forma asíncrona. Consulta GET /api/v1/stories/jobs/:jobId para seguir el progreso.

Cuerpo de la solicitud

CampoTipoEstadoDescripción
promptstringobligatorioDescripción de la historia, máx. 5000 caracteres.
aspectRatiostringopcionalUno de 16:9, 9:16, 1:1, 21:9. Por defecto: 16:9.
renderVideobooleanopcionalfalse: devuelve el primer clip de IA (rápido, ~10 min). true: renderiza todos los clips en un único MP4 compuesto mediante Remotion (~12–15 min).
scopestringopcionalstory (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.
titlestringopcionalNombre visible de la historia.
contentTypestringopcionalCategoría del formato de la historia. Una de: cinematic_story, news_analysis, persona_channel, ad_spot. Por defecto: cinematic_story.
localestringopcionalCódigo de idioma, p. ej. en, zh-TW. Por defecto: en.
seriesIdstringopcionalVincular a una historia existente para mantener la coherencia visual de los personajes.
assetsarrayopcionalThe cast and props for this story — reference assets already in your series library, or attach one-off assets inline. See the table below.
continuityModestringopcionalWith 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.
isFinalebooleanopcionalImplied by continuityMode: "finale"; you rarely need both.
allowMultiStorybooleanopcionalLet 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.
seriesContinuityContextobjectopcionalEscape 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[] opcional

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.

CampoTipoDescripción
seriesAssetIdstringReference an existing library asset. Everything else is optional when this is set.
typestringcharacter · scene · item · reference. Required for an inline asset.
namestringRequired for an inline asset. Also how the writer refers to it — name it in your prompt for it to appear.
descriptionstringOverrides the library description for this story only.
imageUrlstringImage for an inline asset. Re-hosted on our storage.
itemTypestringItems only: prop · product · logo. A logo is composited flat, never given 3D depth.
isHeroProductbooleanad_spot: the advertised product — appears in every clip. Fills heroItemId for you.
isPrimaryBackgroundbooleannews_analysis / persona_channel: the set reused across every shot. ad_spot: the real place (e.g. your shop) used in the shots that name it. Fills primaryBackgroundAssetId for you.
useAsReferenceFramebooleanUse this image as the first frame / reference for image-to-video on clips featuring the asset.
genderstringCharacters only: surfaced to the writer.
agestringCharacters only: surfaced to the writer.
personalityTraitsstring[]Characters only: surfaced to the writer.
timeOfDaystringScenes only, e.g. dusk.
weatherstringScenes only, e.g. rain.
persistbooleanAccrue an inline asset into the series library so later stories can reference it by id. Default false.

Characters, scenes and items only enter the story when the prompt names them — attaching a whole series library does not drag its entire cast into every episode. The two pin flags are the exception: a pinned hero product or primary background is guaranteed regardless of whether the script says its name.

globalConfig opcional

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.

CampoTipoValoresPor defecto
storyInputModestringauto · develop · follow_script (auto decides whether the prompt is an idea to develop or a finished script to follow verbatim)auto
durationSecnumber15–192 (total seconds; about one 8 s clip per 8 s, at least 2 — 16 → 8+8, 20 → 8+6+6; longer content is split into episodes)60
clipCountnumber2–24 clips sharing durationSec, 4–12 s each (16 with 4 = 4 × 4 s); a count that does not fit is refused 400. Omitted: about one 8 s clip per 8 s, or the count the prompt names ("in 4 clips")—
aspectRatiostring16:9 · 9:16 · 1:1 · 21:916:9
resolutionstring480p · 720p · 1080p720p
pacestringslow · normal · fastnormal
structurestringauto · 3_act · 4_act · montageauto
captionStylestringstandard · words — how dialogue captions show: each line whole, or word by word (TikTok style: a few words at a time, the spoken word highlighted). Change it later with MCP update_caption_settings stylewords for 9:16, else standard
qualityTierstringdraft · balanced · premium (premium selects the advanced model lane)balanced
stylePresetstringauto, or a visual style id (list_style_presets kind visual). An unknown id is refused 400; custom style words go in style.cinematicStyleauto
platformTargetstringADVISORY — shown to the writer as context; nothing enforces it. youtube · tiktok · instagram · xyoutube
safetyModestringADVISORY — shown to the writer as context; not a content filter. standard · strictstandard
seedstringany string — shared across clips for continuity (API-only)—
videoModelstringa model id, or autoauto
videoEditModelstringvideo editing only — bytedance/seedance-2.5-video-edit (Seedance 2.5 Video Edit), or auto. The old ids muapi/sd25-video-edit and muapi/sd2-video-edit are still accepted and mean the same model. Omit to keep the existing editing recipe.auto
imageModelstringstoryboard frames + scene/item images — a model id, or auto (GPT Image 2.5)auto
characterImageModelstringcharacter portraits, sheets and appearance variants — auto (GPT Image 2.5), google/nano-banana-2 or bytedance/seedream-5-pro (list_models: character)auto
modelStrategystringauto · fixed · cost_optimized · quality_first · hero_premiumauto
generationModestringDEPRECATED — ignored. The selected model determines frame or reference inputs.auto
localestringen · zh-TW · ja · ko · th · id (also accepted top-level)en

contentTypeConfig opcional

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.

contentTypeCampos
cinematic_story(no per-type fields — format craft is built in)
news_analysisprimaryBackgroundAssetId, primaryBackgroundName (pin a studio scene across anchor shots)
persona_channelprimaryBackgroundAssetId, primaryBackgroundName (pin the persona's set)
ad_spotheroItemId, heroProductName, heroTagline, heroKeyBenefit, primaryBackgroundAssetId, primaryBackgroundName (the advertised product, and optionally the real place it is sold or used). heroItemId is an item of the seriesId series: its name fills an empty heroProductName (the tagline is never invented from its description) and the item joins the story with its picture. An id that is not an item of that series answers 400 hero_item_not_found (allowed lists the series items); heroItemId without seriesId answers 400 hero_item_needs_series. With no hero given, the series item flagged isHeroProduct is used, else the series product the prompt names, else the first product. Other series products join the story only when the prompt names them or a shot shows them.

Otros campos opcionales

CampoTipoDescripción
storyQualityConfigobject{ characterStyle?: "match_story" | "keep_own", characterStyleOverrides?: { [character name or series asset id]: "match_story" | "keep_own" }, directClipImageGeneration?: boolean }. characterStyle (default match_story, or the series default): a cast member whose own look is another style is redrawn in the story style, or keeps its own look. Hired characters always keep their own look. enableStyleAdaptedCharacters is deprecated (false reads as keep_own).
castArtbooleanscope "story" only. Default true, as the app: once the text is written, cast art (character sheets and shared backgrounds) starts and is charged automatically. false: draw it yourself with POST /assets. Every clip video needs it (409 ASSETS_REQUIRED otherwise). scope "full" always draws it.
styleobjectStyle-consistency overrides: cinematicStyle, era, colorPalette, lightingStyle, filmGrain, lensCharacter, materialEmphasis, styleCategory, negativeHints, stylePreset.
seriesContinuityContextobjectSeries continuity context for episodic generation. Used together with seriesId.

Fields that aren't part of the selected contentType's schema are silently ignored — cinematic_story takes no per-type fields at all, so pass contentTypeConfig only for ad_spot, news_analysis, or persona_channel.

Ejemplo de control total
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"
    }
  }'

Respuesta 202 Accepted

{ "jobId": "cmnk8wu2q0001q3vpmqv93nmy", "status": "queued", "pollUrl": "/api/v1/stories/jobs/..." }

Endpoints paso a paso

After generating a story with scope: "story", use these endpoints to generate storyboard images, clip videos, and the final rendered MP4 as separate steps.

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

Generate the cast and world: character portraits + 2×2 character sheets, plus scene and item images. Run this before /storyboard for the best face consistency. Skips assets that already have images. 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 }.

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

Generate storyboard grid images for all clips. Skips clips that already have images. 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.

Credits: the batch quote over the planned clips | Scope: stories:generate
POST/api/v1/stories/:storyId/videos

Generate video for each clip. Skips clips that already have a video URL. Runs as the app's own Generate All: one clip at a time in story order, each continuing the previous clip's video (parallel: true runs a few at once without that reference), each clip charged, settled and refunded on its own within the batch price. Like Generate All, clipIds narrows it to those clips, and with includeExisting: true they are re-generated even when they have a video. Answers 202 { runId, runUrl, cancelUrl, clipIds, estimatedCredits }: poll GET /api/runs/:runId until it ends (result.results lists each clip); POST /api/v1/runs/:runId/cancel stops it. /clips-all takes the same options.

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

Stitch all clip videos + music + captions into a final MP4. Every clip of the cut needs its video first: otherwise it answers 409 clips_missing_video with missingVideoClips (clipId, position, title, hasImage) and charges nothing.

Credits: 10 | Scope: stories:generate

Un clip cada vez

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

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

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

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

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

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

Rebuild ONE clip end to end — a fresh 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.

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

Clip authoring supports description for scene and image context, and optional motionDescription for video movement and timed beats. Video generation uses both. When motion is empty, video generation automatically creates and saves timed motion first. Send null to prepare new motion on the next video run. Draw a multi-panel continuity reference sheet for one clip. The panels are derived from the clip's time beats — an authored beat track when motionDescription carries one, otherwise the beat plan its generation length implies — so the sheet mirrors that shot rather than a fixed set of generic moments. Two panels per beat, snapped to a supported grid layout. Starts a run and answers 202 with runId and runUrl; poll GET /api/runs/:runId until status is terminal, then read result.status: completed with the grid URL in result.imageUrl (also saved on the clip), or failed with result.error (refunded). The balance is checked before the run starts: too few credits answers 402 insufficient_credits, and a start the platform refuses answers 503 workflow_start_failed with nothing charged.

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

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

Draw (or redraw) ONE character, scene or item from its description, or re-derive a character's 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.

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

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

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

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

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

Write a NEW storyline branching off an existing clip — an alternative continuation the story keeps alongside the original, which is untouched. Required body: branchFromClipId. Optional prompt (where the story goes) and clipCount (4–24). The new clips and their images are made within the price; video is not, so animating them is a separate, separately-priced step. Runs as the editor's own continuation (a story run): answers 202 { jobId, runId, runUrl, pollUrl }; poll the job (or GET /api/runs/:runId) until it ends. A paid-value price above X-Max-Generation-Credits answers 409 with the quote and starts nothing.

Credits: the editor's continuation quote, settled to cost | Scope: stories:generate
GET/api/v1/stories/:storyId/renders

The story's finished videos, newest first, with latestSuccessful called out so a failed render is never mistaken for the current cut. A story keeps every render. Optional ?limit (max 50). Each render has width, height and duration (seconds; API renders), aspectRatio, the story's locale and its fileName (API renders are named <title>_<aspect>_<language>.mp4, e.g. glow-cola_9x16_zh-TW.mp4). API renders run in their own pool (2 at once per account, 3 across accounts) and never take the app's export slots; a render past the pool waits its turn (the job reads queued, phase waiting_for_render_slot).

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

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

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

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

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

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

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

Make a new version of your story in another language, aspectRatio (16:9, 9:16, 1:1, 21:9) and/or stylePreset. Every clip keeps its media; clips are cropped or fitted to the new frame and text layers re-laid out. A language change finds where each line is spoken and translates the script by itself. A format or style version is free and made at once (201). A language version first answers a quote (200, status: "quote"): translationCredits (the copy dialog's price, charged when the version is made), laterSteps (what the paid steps after it will ask) 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.

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

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

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

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

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

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

curl -X POST https://www.yoh.app/api/v1/stories/STORY_ID/clips/CLIP_ID/video \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"videoModel":"bytedance/seedance-2.0"}'
# Poll GET /api/runs/RUN_ID until completed → the clip's videoUrl is replaced

Editar la historia

Direct the story instead of only re-rolling it: rewrite a shot, retime it, add a beat, reorder, or cut one. These are immediate (no job to poll) and cost no credits. A clip's description provides scene and image context; optional motionDescription provides video choreography. Video uses both — edit the appropriate field, then call that clip's /image or /video to re-shoot it with the new direction. Access follows the editor: anyone with edit access to the story (its owner, a series collaborator with edit rights, a job's creator) may edit it with their own key; others get 404.

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

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

Media columns (imageUrl, videoUrl, generation history) are deliberately not writable — those belong to the pipelines. Use the clip's /image and /video endpoints to change them.

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

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

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

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

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

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

A talking clip's audio sync (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.

Credits: 0 | Scope: stories:read (GET), stories:write (POST)
PATCH/api/v1/stories/:storyId/clips

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

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

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

Credits: 0 | Scope: stories:write
Reescribir una toma y volver a generarla
# 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"
Ejemplo paso a paso
# 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 → videoUrl

Consultar el estado de la tarea

Obtener el estado de la tarea

GET/api/v1/stories/jobs/:jobIdscope: stories:read

Consulta este endpoint después de llamar a POST /api/v1/stories/generate. La tarea suele tardar entre 8 y 15 minutos, según la longitud de la historia y si renderVideo está activado. Recomendamos consultar cada 15–30 segundos.

Ciclo de vida del estado de la tarea

queuedgenerating_storygenerating_assetsgenerating_videosrendering_videocompletedofailed

Which statuses you see depends on the scope you requested — a story job goes straight from generating_story to completed. A transient failure sends the job back to queued with error set to a retry message; that is not terminal, so treat only completed and failed as final states.

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

Reading a job (changed 2026-10-20). A story job that rendered nothing (no render scope, no renderVideo) answers videoUrl: null; its first clip's video is firstClipVideoUrl, never the finished film. A failed clip image job answers failure { category, reason, message } like a clip video. A failed or cancelled job whose refund has not been settled yet says settlement: "pending". A 402 insufficient_credits carries required, balance and topUpUrl.

Campos de la respuesta

CampoTipoDescripción
jobIdstringIdentificador único de la tarea.
statusstringEstado actual. Consulta el ciclo de vida más arriba.
progressnumberEntero de 0 a 100 que indica el porcentaje completado.
storyIdstring|nullSe rellena cuando termina la generación de la historia. Úsalo para obtener los recursos.
renderVideobooleanIndica si se solicitó un MP4 final compuesto.
videoUrlstring|nullURL absoluta del vídeo cuando el estado es completed. Puede ser un MP4 compuesto (renderVideo: true) o el vídeo de IA del primer clip (renderVideo: false).
errorstring|nullMensaje de error legible. Se rellena cuando falla y también mientras la tarea se reintenta, en cuyo caso el estado vuelve a queued. Siempre es null si tiene éxito.
waitingFor / messagestring (optional)Aparece mientras la tarea espera el resultado de un proveedor (el estado sigue en queued y error es null): el valor es "provider" y message indica qué está esperando. Sigue consultando.
executionobject (optional)La ejecución de Workflow a la que pertenece la tarea: { version, runId, phase, heartbeatAt, status }. Síguela en GET /api/runs/:runId (o su /stream). No aparece en tareas antiguas.
createdAt / startedAt / completedAtstring (ISO)Marcas de tiempo.
runId / runUrlstring (202 only)En la respuesta 202 de cada inicio de tarea (no en esta consulta): la ejecución iniciada para la tarea y su URL de estado. Su uso es opcional; basta con consultar este endpoint.

Historias

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.

Listar historias

GET/api/v1/storiesscope: stories:read

Devuelve una lista paginada de tus historias.

Parámetros de consulta

ParámetroPor defectoDescripción
page1Número de página (≥ 1).
limit20Resultados por página (1–100). Capped at 100.
status—Filtrar por estado de la historia.

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

Obtener una historia

GET/api/v1/stories/:storyIdscope: stories:read

Devuelve todos los metadatos de una historia, incluido el recuento de sus personajes, escenas, objetos y 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"
}

Resumen de recursos

Cuando termina una tarea, puedes obtener todos los recursos generados para la historia con el storyId que devuelve la tarea. Todos los endpoints de recursos requieren el permiso assets:read.

GET /api/v1/stories/:id/characters

Personajes — nombre, descripción, rol, imageUrl, información de voz

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

Escenas — descripción, momento del día, clima, iluminación, imageUrl

GET /api/v1/stories/:id/items

Atrezo / objetos — descripción, importancia, imageUrl

GET /api/v1/stories/:id/clips

Clips — narración, duración, videoUrl, imageUrl, audioUrl

Personajes

Listar personajes

GET/api/v1/stories/:storyId/charactersscope: assets:read
{
  "data": [{
    "id": "char_abc123", "name": "Chef Axon", "role": "protagonist",
    "description": "A robot chef with a warm personality",
    "imageUrl": "https://cdn.example.com/characters/chef-axon.jpg",
    "voiceName": "Neutral-EN", "order": 0
  }]
}

Clips y vídeos

Listar clips

GET/api/v1/stories/:storyId/clipsscope: assets:read

Cada clip es un segmento de escena con su propio vídeo, imagen fija y audio generados por IA. Todos los campos de URL son absolutos y de acceso público.

{
  "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"
  }]
}

Cuando renderVideo es false, el videoUrl de la tarea apunta al vídeo del primer clip. Usa este endpoint para obtener la URL del vídeo de cada clip.

Biblioteca de recursos de la historia

Sube un recurso una vez, úsalo para siempre

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

La misma biblioteca en la que escribe el panel de recursos del Studio. Sube un personaje, fondo, objeto o referencia de estilo, guarda el ID devuelto y haz referencia a él al generar pasando ese ID como seriesAssetId en el array de recursos; no es necesario crear un nuevo enlace de imagen por vídeo. Las imágenes que envías se alojan en nuestro almacenamiento, por lo que el ID sigue funcionando aunque tu URL de subida caduque.

CampoTipoDescripción
typestringcharacter, scene, item o reference.
namestringNombre visible. También es la clave con la que coincidirán las generaciones posteriores.
imageUrlstringURL pública de la imagen. Alternativamente, envía imageBase64.
itemTypestringSolo objetos: prop, product o logo. Un logo se compone de forma plana, nunca se le da profundidad 3D.
isHeroProductbooleanSolo objetos: el producto sobre el que trata este anuncio. Aparece en cada clip.
isPrimaryBackgroundbooleanSolo escenas: el estudio o set reutilizado en cada toma de presentador.
seriesDuplicatestringSolo personajes, tras preguntar al usuario: "new" añade un segundo personaje con un nombre que la historia ya tiene.
  • Envía un recurso o un array "assets" para insertar o actualizar hasta 100 a la vez.
  • Por defecto, los recursos se fusionan si el nombre coincide exactamente, por lo que volver a subir el mismo catálogo es idempotente, excepto con un personaje cuyo nombre ya exista en la historia: eso devuelve 409 SERIES_NAME_EXISTS con el personaje existente y no guarda nada. Pregunta al usuario y luego envía seriesDuplicate "new" para añadir un segundo personaje, o pasa ?mergeStrategy=exact para actualizar el existente. ?mergeStrategy=fuzzy tolera cambios de nombre; insert siempre crea un nuevo recurso.
  • DELETE devuelve 409 SERIES_ASSET_IN_USE con los episodios que aún usan el recurso (stories: id, title, clipCount) y no elimina nada. Pregunta al usuario y luego envía confirm: true (en el cuerpo o ?confirm=true): los episodios conservan su propia copia y sus clips no cambian. Un personaje con una contratación abierta devuelve 409 SERIES_ASSET_HIRED (hires) y no se puede eliminar hasta que la contratación termine.
  • Establece isHeroProduct en un objeto (ad_spot) o isPrimaryBackground en una escena (news_analysis / persona_channel) para fijarlo. Al generar, el marcador se resuelve automáticamente; nunca tendrás que escribir heroItemId a mano.
PermisoDa acceso a
GET/api/v1/series
POST/api/v1/series
GET/api/v1/series/:seriesId
PATCH/api/v1/series/:seriesId
GET/api/v1/series/:seriesId/assets?type=character
POST/api/v1/series/:seriesId/assets
PATCH/api/v1/series/:seriesId/assets/:assetId
DELETE/api/v1/series/:seriesId/assets/:assetId
DELETE/api/v1/series/:seriesId/assets/:assetId/looks/:lookId
POST/api/v1/series/:seriesId/assets/:assetId/looks/:lookId
POST/api/v1/uploads
PATCH/api/v1/stories/:storyId/characters/:assetId
PATCH/api/v1/stories/:storyId/scenes/:assetId
PATCH/api/v1/stories/:storyId/items/:assetId
POST/api/v1/stories/estimate
GET/api/v1/credits
GET/api/v1/models
POST/api/v1/stories/:storyId/swap-asset
POST/api/v1/series/:seriesId/uploads
POST/api/v1/stories/:storyId/upload
GET/api/v1/stories/:storyId/upload
POST/api/v1/uploads/presign
GET/api/v1/stories/:storyId/media

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

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

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

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

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

1 — crear la historia (una vez)
curl -X POST https://www.yoh.app/api/v1/series \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"Acme brand films","styleGuide":{"cinematicStyle":"clean product cinematography"}}'
# → { "series": { "id": "series_abc", ... } }
2 — subir el logo (una vez) y guardar el id
curl -X POST https://www.yoh.app/api/v1/series/series_abc/assets \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "type": "item",
        "name": "Acme Logo",
        "itemType": "logo",
        "description": "Wordmark, orange on white",
        "imageUrl": "https://your-admin.example.com/tmp/logo.png"
      }'
# → { "asset": { "id": "series-item-...", "imageUrl": "https://...blob..." } }
# That imageUrl is ours now — your temporary link can expire.
3 — generar un anuncio que lo incluya (cada vez)
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.

Recursos de la historia

Registrar una imagen de personaje para una historia

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

POST /api/v1/series-assets/ensure-character-image sigue funcionando y no ha cambiado, pero solo gestiona personajes. Es preferible usar este endpoint.

Sube y guarda una imagen de referencia de personaje para una historia, de modo que la IA mantenga la misma cara y el mismo estilo en todos sus episodios. Esta operación es idempotent: puedes llamarla varias veces con el mismo nombre de personaje sin problema.

CampoTipoDescripción
seriesIdstringID de la historia a la que se asociará el personaje.
characterNamestringEl nombre del personaje (se usa como clave única dentro de la historia).
imageUrlstringURL de acceso público de la imagen de referencia del personaje.

Publicar en redes sociales

Envía un vídeo terminado directamente a una cuenta conectada, para que una automatización pueda gestionar generar → renderizar → publicar con una sola credencial. Conecta primero las cuentas en la app; este endpoint publica en ellas.

Publicar un vídeo

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

Devuelve 202 con { postId, status: "publishing" } y sube en segundo plano; consulta el estado en el endpoint de abajo. Pasa wait: true para bloquear hasta que termine la subida (puede tardar minutos). La URL del vídeo debe coincidir con un renderizado correcto o con el vídeo original subido de esta historia. Los streams de YouTube están restringidos a direcciones públicas y al límite de subida existente de 512 MB.

CampoTipoEstadoDescripción
socialAccountIdstringobligatorioUna cuenta conectada que pertenece al usuario de tu clave.
storyIdstringobligatorioUna historia que pertenece al usuario de tu clave.
videoUrlstringobligatorioURL pública del MP4: el videoUrl de una tarea completada.
platformstringobligatoriofacebook · instagram · tiktok · threads · youtube
captionstringopcionalTexto de la publicación.
youtubeobjectopcional{ title, privacyStatus, categoryId, tags } — solo para YouTube.
durationSecnumberopcionalSe usa para la validación previa de la plataforma.
aspectRatiostringopcionalSe usa para la validación previa de la plataforma.
fileSizeBytesnumberopcionalSe usa para la validación previa de la plataforma.
waitbooleanopcionalBloquea hasta que se complete la subida. Por defecto es false.
curl -X POST https://www.yoh.app/api/v1/social/publish \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "socialAccountId": "acct_123",
    "storyId": "STORY_ID",
    "videoUrl": "https://cdn.example.com/video/final.mp4",
    "platform": "youtube",
    "caption": "Episode 1 is live",
    "youtube": { "title": "The Lighthouse — Ep 1", "privacyStatus": "public" }
  }'

Consultar estado de publicación

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

Ciclo de vida: publishing → published | failed.

Cuentas gestionadas

Para plataformas que dan servicio a muchos clientes. En lugar de una clave y un fondo de créditos para todos —donde un cliente muy activo agota el saldo de los demás y nadie sabe quién ha gastado qué—, crea una cuenta por cliente y financia cada una desde tu propio saldo. Tus clientes nunca se registran aquí ni nos pagan a nosotros; la relación comercial sigue siendo tuya.

Crear una cuenta

POST/api/v1/accountsscope: accounts:manage

externalId es tu propio ID para el cliente. Si llamas dos veces con el mismo, se devuelve la cuenta existente en lugar de crear otra, así que reintentar la solicitud es seguro. La clave de API se devuelve una sola vez, al crearla por primera vez; guardamos un hash y no podemos volver a mostrarla, así que guárdala al recibirla. Una cuenta nueva empieza con saldo cero.

CampoTipoDescripción
externalIdstringObligatorio. Tu ID para este cliente (por ejemplo, un ID de inquilino). Único por plataforma.
namestringEtiqueta opcional, para tus propios informes.

Reemplazar la clave de una cuenta gestionada

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

Si se ha perdido la clave inicial, llama a este endpoint con la clave de la plataforma propietaria y sin cuerpo en la solicitud. Usa el ID de la cuenta o el externalId. Revoca de forma atómica las claves anteriores de la cuenta y devuelve una nueva apiKey una sola vez (201). Guárdala de forma segura. Repetir esta llamada vuelve a rotar la clave; los reintentos normales de creación de cuenta nunca reemplazan una clave que funcione.

Financiar una cuenta

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

Mueve créditos de tu propio saldo al de la cuenta; es una transferencia, no una concesión, por lo que solo puedes repartir lo que hayas comprado realmente. Devuelve 402 cuando tu propio saldo no puede cubrirlo. :accountId acepta tanto nuestro ID de cuenta como tu externalId.

CampoTipoDescripción
amountintegerObligatorio. Créditos a mover. Debe ser un valor positivo.
idempotencyKeystringPasa tu ID de factura o de periodo de suscripción. Si se reintenta o se reenvía la llamada, se devolverá la transferencia original en lugar de financiar la cuenta dos veces. Reintenta con la misma clave, destinatario e importe. Una repetición no transfiere créditos de nuevo, incluso si el saldo de la plataforma se ha agotado. Cambiar el destinatario o el importe devuelve 409. Las claves demasiado largas se rechazan en lugar de truncarse.
descriptionstringNota opcional que se muestra en ambos lados de la transferencia en el historial de créditos.

GET /api/v1/accounts enumera tus cuentas con sus respectivos saldos y gasto total; GET /api/v1/accounts/:accountId/credits lee una. La clave propia de una cuenta gestionada solo tiene permisos de generación; nunca puede crear ni financiar cuentas por sí misma.

Claude Code y Codex (MCP)

Yoh incluye un servidor MCP para que los agentes de IA (Claude Code, Codex, Claude Desktop o cualquier cliente MCP) puedan crear y editar historias en tu cuenta: escribir una historia nueva a partir de una idea, añadir sus personajes y generar el vídeo, o cambiar el contenido y el diálogo de los clips, mover y recortar elementos en la línea de tiempo y añadir superposiciones libres. Todo lo que gasta créditos espera siempre tu aprobación explícita. Sigue las mismas reglas que el editor del navegador, así que una edición hecha desde la terminal queda exactamente donde la dejaría un arrastre.

Tres formas de usarlo

Elige la que mejor se adapte a las herramientas de tu agente. La skill yoh de abajo enseña las tres.

  • MCP + navegador (recomendado en Codex, Claude Code y las apps de Claude) El agente hace el trabajo con estas herramientas MCP y maneja una pestaña del navegador a su lado (Claude in Chrome, el navegador de Codex): tras cada paso, abre el enlace que trae la respuesta (la tabla del guion tras una historia nueva, el panel del clip tras el vídeo de un clip, la lista de Generar todo mientras hay una ejecución en curso, el editor avanzado para la línea de tiempo y las capas de texto, el MP4 del renderizado) y comprueba el resultado en la página real. Inicia sesión una vez en ese navegador con la misma cuenta que la clave de API. El MCP y la página comparten los mismos datos: la página del episodio se actualiza sola a los pocos segundos de un cambio por MCP, y el agente recarga el editor avanzado tras un cambio en la línea de tiempo. Cada gasto pasa por una sola vía: el presupuesto del MCP o el diálogo de precio de la app.
  • Solo navegador (sin clave de API) El agente usa las páginas de yoh.app en tu navegador, igual que lo harías tú: Crear → Historia, la tabla del guion y el panel del clip, el editor de personajes y su selector de modelo, Generar todo, el editor avanzado, las versiones y la exportación. Cada gasto pasa por el propio diálogo de precio de la app, y el agente nunca escribe datos de pago ni tu código de inicio de sesión.
  • Solo MCP (headless) Para scripts, CI y agencias que hacen muchas variantes: quote → confirmToken en cada gasto, consulta get_job / get_run, detén con cancel_generation, crea variantes con create_story_versions y generate_all_clips, y descarga el videoUrl de cada renderizado terminado. No hace falta navegador.

Ejemplo en todos los modos: un anuncio de 16 segundos en 9:16 en inglés y japonés, escrito en una historia con su producto, generado, versionado al japonés y renderizado dos veces.

Conectar

Recomendado: inicia sesión con Yoh, sin clave de API. Usa exactamente esta dirección (yoh.app sin www redirige y el inicio de sesión comprueba la dirección). En Claude Code:

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

Luego escribe /mcp, elige yoh, pulsa Authenticate, inicia sesión en Yoh en el navegador que se abre con la cuenta que quieras usar y aprueba.

En Codex (CLI, extensión del IDE o la app de escritorio de ChatGPT), añade el servidor e inicia sesión; después pon tool_timeout_sec = 300 en [mcp_servers.yoh] de ~/.codex/config.toml y reinicia Codex:

codex mcp add yoh --url https://www.yoh.app/api/mcp
codex mcp login yoh
  • App de escritorio de Claude o claude.ai: Customize → Connectors → + Add → Add custom connector, llámalo Yoh, pega la dirección y luego Connect e inicia sesión. Actívalo en cada chat desde + → Connectors. En planes Team o Enterprise, primero lo añade un propietario.
  • Cursor: añade yoh con esta dirección a ~/.cursor/mcp.json e inicia sesión cuando Cursor lo pida.
  • ChatGPT: Plugins → + → Add custom MCP server, llámalo Yoh, pega la dirección e inicia sesión. Algunos planes necesitan que un administrador permita servidores propios.

Compruébalo: pide a tu agente que ejecute get_credits; debería mostrar tu saldo de Yoh. Puedes ver y desconectar las apps conectadas en Ajustes → Claves de API → Apps conectadas.

Para que tu agente vea las páginas que cambia: Claude Code con claude --chrome (la extensión Claude in Chrome), la app de escritorio de Claude con el conector Claude in Chrome, o Codex en la app de escritorio de ChatGPT con su navegador. El skill le dice al agente cuándo mirar.

O usa una clave de API (scripts, CI, apps sin inicio de sesión)

Crea una clave de API en /api-keys. Para crear historias nuevas y generar vídeo, incluye stories:generate; para editar sin gastar, bastan stories:read y stories:write. Después, en Claude Code:

claude mcp add --transport http yoh https://www.yoh.app/api/mcp \
  --header "Authorization: Bearer sk_live_YOUR_API_KEY"

O añade al repositorio una configuración de proyecto para que todo el equipo tenga la conexión al abrirlo; cada miembro aporta su propia clave mediante env:

.mcp.json
{
  "mcpServers": {
    "yoh": {
      "type": "http",
      "url": "https://www.yoh.app/api/mcp",
      "headers": { "Authorization": "Bearer ${YOH_API_KEY}" },
      "timeout": 900000
    }
  }
}

En Codex, añade el servidor a ~/.codex/config.toml y configura YOH_API_KEY en tu shell:

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

Instala la skill de Yoh

La skill enseña a tu agente a crear y editar bien una historia de Yoh: empezar en una serie, presupuestar antes de gastar y mirar los clips antes de juzgarlos. Es un solo archivo, al día con el servidor. Ejecuta la línea de tu agente, o descargar SKILL.md

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

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

Luego solo tienes que pedirlo: «haz una historia de 60 segundos sobre una enfermera del turno de noche que oye fantasmas» o «ajusta el ritmo del episodio 3: recorta los silencios, encaja los subtítulos en los tiempos y vuelve a renderizar». Ejecuta /mcp en Claude Code o Codex para comprobar la conexión.

Qué puede hacer el agente

HerramientasPermisoEjemplos
Leerstories:readlist_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 adjunta la imagen del storyboard y, con withFrames, fotogramas reales extraídos del vídeo generado; es la única forma de que el agente vea cómo queda realmente un clip. get_asset_images hace lo mismo con el reparto, los escenarios y el atrezo.
Template versionsstories:write / stories:read / assets:readcreate_template_version · get_template_version · list_template_destinations · list_template_assets · update_template_version · analyze_template_version · get_template_analysis · accept_template_adaptation · quote_template_version · commit_template_version · cancel_template_version · fork_template_versionMap existing or newly created series assets, review wording and language, then create a free draft. REST families: /api/v1/template-version-sessions/:sessionId and /api/v1/template-version-runs/:runId. Template REST OpenAPI schemas and endpoints
Template generation ★stories:generategenerate_template_versionQuote the run first, show the maximum credits, then confirm. Poll get_template_version for persisted progress and recovery.
Crear ★stories:generatecreate_storyEscribe una historia totalmente nueva a partir de un prompt. estimate_story es gratis y presupuesta un plan completo por adelantado, algo que los presupuestos por acción no pueden hacer. Empieza con scope="story" (solo texto) en lugar de "full", que cobra el vídeo de inmediato.
Start from a videostories:write / stories:readupload_video · finish_video_upload · get_video_upload · upload_file · list_story_mediaFree. upload_video makes a draft episode in a series and answers one curl command that sends the local file straight to storage; finish_video_upload verifies it, fills the details from the video, picks a cover and cuts it into editable clips; get_video_upload reports the cut until it is ready. Open links.upload to watch it. upload_file hosts a song, logo or extra footage the same way (for an overlay src or an asset picture); list_story_media lists every file of a story to download and check.
Story versions ★stories:generate / stories:readcreate_story_version · create_story_versions · get_story_version · get_version_translation · edit_version_translation · list_story_versionsA language, format or style version keeps every clip's media. A format or style version is free; a language version's translation is charged when it is created, so it answers a quote first and is made with confirmToken (the copy dialog's price). create_story_versions makes many at once the cheap way: each new style or aspect once, then its languages from it after its visual step. create_story_version(s) need stories:generate, edit_version_translation stories:write; the reads need stories:read. The paid steps run with run_story_version_step (stories:generate); its free 'prepare' step restarts a failed translation.
Historiasseries:read / series:write assets:read / assets:writelist_series · get_series · create_series · update_series · list_series_assets · add_series_asset · update_series_asset · delete_series_asset · remove_series_look · use_series_look_as_mainLa capa por encima de una historia: los personajes, fondos y estilo compartidos que se mantienen entre episodios. Vincula una historia nueva con el seriesId de create_story para heredarlos.
Clips y diálogostories:writeupdate_clip · update_clips · set_all_clips · reorder_clips · add_clip · delete_clip · duplicate_clip · reorder_clip · set_clip_duration · select_clip_version · select_clip_image_version · add_dialogue_line · update_dialogue_line · remove_dialogue_line · hide_dialogue_line · add_audio_fx · update_audio_fx · remove_audio_fx · update_story · update_caption_settingsSi el diálogo no cabe en su clip, la duración del clip aumenta automáticamente. Las ediciones de texto nunca cambian el contenido multimedia ya generado. update_clips edita muchos clips a la vez, cada uno con sus propios valores; set_all_clips aplica un mismo valor a todos los clips y rechaza el texto, porque la misma descripción en todas partes deja plano el episodio. Ambos rechazan el lote entero si alguna entrada es errónea. reorder_clips fija todo el orden de reproducción en una sola llamada.
Asistente de Yohstories:writeask_assistant · apply_assistant · undo_assistantPasa una solicitud al propio asistente de Yoh, el que está detrás de «Preguntar a la IA» en la página del guion y del agente de IA del editor. ask_assistant solo propone: las ediciones vuelven como cambios y los pasos de pago, como presupuestos. apply_assistant aplica los cambios que conserves; uno de pago necesita además el confirmToken de su presupuesto cuando el usuario lo acepta. undo_assistant deshace lo que cambió una ejecución aplicada. Cada turno de ask_assistant cuesta unos pocos créditos: su coste real de IA, hasta un pequeño presupuesto por turno.
Línea de tiempostories:writemove_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_soundtrackAl mover un clip se desplazan todos los clips posteriores; al cambiar el tamaño se ancla el inicio, así que arrastrar el inicio recorta la entrada. Los elementos de la historia no se pueden eliminar ni dividir: ocúltalos o expórtalos a una superposición libre.
Superposicionesstories:writeadd_overlay · update_overlay · move_overlay · resize_overlay · split_overlay · duplicate_overlay · delete_overlay · delete_freeform_row · upload_mediaNo puede haber dos superposiciones en la misma fila; si hay conflicto de posición, se rechaza y se listan las filas libres. El contenido multimedia necesita un src https alojado.
Estilostories:writeapply_overlay_preset · set_caption_styleLos ajustes predefinidos de texto del editor, las plantillas de subtítulos y unas 32 animaciones de entrada y salida; así el agente aplica lo que ofrece el producto en lugar de inventar CSS. El estilo de los subtítulos se aplica por línea de diálogo.
Personajes y escenariosstories:writeadd_character · update_character · delete_character · add_scene · update_scene · delete_scene · add_item · update_item · delete_item · keep_character_sheetEliminar un personaje lo quita de todos los clips. Sus líneas de diálogo se conservan sin hablante hasta que elijas quién las dice (update_dialogue_line); un papel con una cara bajo licencia o con una contratación pendiente no se puede eliminar.
Casting de vozstories:writeset_character_voiceVoces de catálogo filtradas por el idioma de la historia, más las voces clonadas listas de la cuenta. El casting es gratis; el diálogo ya interpretado conserva la voz con la que se generó.
Regenerar vozstories:writeset_revoice_enabled · select_revoice_take · undo_revoice · fix_audio_syncCambiar entre las nuevas tomas y la interpretación original es gratuito y reversible; las tomas se guardan en ambos casos.
Generación ★stories:generateregenerate_clip · regenerate_clip_image · edit_clip_image · enhance_clip_image · generate_clip_video · edit_clip_video · swap_asset · revoice_clip · regenerate_character_sheet · regenerate_asset_image · generate_all_asset_images · generate_soundtrack · generate_storyboard · generate_all_videos · generate_all_clips · generate_clip_grid_reference · continue_story_from_clip · run_story_version_step · render_storyCada par es editar frente a redibujar: edit_clip_image reescribe el fotograma actual a partir de una instrucción, mientras que regenerate_clip_image dibuja uno nuevo; edit_clip_video es de vídeo a vídeo y mantiene el sujeto, la composición y el movimiento, mientras que generate_clip_video vuelve a generar a partir de la imagen del storyboard. enhance_clip_image ajusta las caras para que se parezcan a las fichas de personaje. regenerate_clip hace imagen y luego vídeo en una sola tarea; es lo adecuado tras cambiar la descripción o el reparto de un clip. generate_clip_grid_reference dibuja una hoja de continuidad cuyos paneles salen de los tiempos del propio clip, no de un conjunto fijo de momentos genéricos.
Stopstories:writecancel_generationThe app's Stop for a jobId or a batch runId: nothing further starts, work in flight finishes and is kept, the rest is refunded. Free.
Hired character looksassets:read stories:writelist_hire_looks · choose_hire_looksThe creator's looks a hired character brings (main sheet, earlier style sheets, the creator's looks) and which are in the episode; choose_hire_looks adds or removes them, the same as the hire page. Names only until the hire is paid. Free.

El gasto tiene doble validación Una herramienta con ★ nunca gasta en su primera llamada: devuelve un presupuesto de créditos y un confirmToken firmado, y el agente debe mostrarte el presupuesto y proceder solo con tu aprobación explícita. Por separado, una clave sin stories:generate físicamente no puede gastar; crea una clave de solo edición con stories:write si quieres un agente de lectura y edición.

Cómo se mantiene seguro y correcto

  • El servidor aplica las reglas del editor (desplazamiento de clips, límites de recorte, pistas de subtítulos, filas de superposiciones) y, ante una infracción, responde con el rango permitido en lugar de ajustar el valor en silencio.
  • El agente lee el conjunto completo de reglas del recurso yoh://guide/timeline, y el prompt /mcp__yoh__edit-story le proporciona un flujo de trabajo seguro.
  • La generación es asíncrona: la mayoría de las herramientas devuelven un runId que el agente consulta con get_run; unas pocas (create_story, render_story, imágenes de personajes) devuelven un jobId para get_job, igual que la API REST.
  • Establece un tiempo de espera largo en .mcp.json (por ejemplo 900000) para que un renderizado no agote el tiempo de espera en el lado del cliente.

Verifica tu conexión

El repositorio incluye un smoke test. Usa el protocolo real, pone a prueba las reglas y demuestra que el control de gasto funciona: nunca gasta créditos y su única escritura se revierte:

node scripts/mcp-smoke.mjs --url https://www.yoh.app/api/mcp --key sk_live_YOUR_API_KEY
# read-only variant:
node scripts/mcp-smoke.mjs --url https://www.yoh.app/api/mcp --key sk_live_YOUR_API_KEY --skip-write

Gestionar claves de API

Los endpoints de gestión de claves de API usan session authentication (la cookie de la sesión iniciada), no una clave de API. Usa la API Keys page para hacerlo desde el navegador, o llama a estos endpoints mediante programación desde tu backend con una sesión válida. /api-keys

Listar claves de API

GET/api/v1/api-keysscope: session
{
  "data": [{
    "id": "key_abc123", "name": "Production",
    "keyPrefix": "sk_live_7WH3",
    "scopes": ["stories:generate", "stories:read", "assets:read"],
    "revokedAt": null, "createdAt": "2026-04-03T10:00:00.000Z"
  }]
}

Crear una clave de API

POST/api/v1/api-keysscope: session
// Response — full key shown once
{
  "id": "key_abc123",
  "key": "sk_live_...",
  "name": "My integration",
  "keyPrefix": "sk_live_7WH3",
  "scopes": ["stories:generate", "stories:read", "assets:read"]
}

Revocar una clave de API

DELETE/api/v1/api-keys/:idscope: session
{ "id": "key_abc123", "revoked": true }

Errores

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.

La mayoría de los endpoints devuelven un objeto error con un code legible por máquina. Los endpoints especializados pueden devolver un error de cadena o un código de nivel superior; comprueba siempre el estado HTTP.

{ "error": { "code": "insufficient_credits", "message": "...", "status": 402 } }
HTTPcódigoSignificado
401unauthorizedClave de API ausente o no válida.
403forbiddenLa clave de API no tiene el permiso requerido.
404not_foundEl recurso solicitado no existe.
400bad_requestCuerpo de la solicitud o parámetros no válidos.
402insufficient_creditsCréditos insuficientes para realizar la operación.
429rate_limitedDemasiadas solicitudes. Comprueba la cabecera Retry-After.
500internal_errorError inesperado del servidor.

Límites de solicitudes: Los endpoints de generación permiten 5 solicitudes por minuto por clave de API. Las lecturas y las ediciones de clips (que no gastan créditos) permiten 60. Cuando alcances un límite, la respuesta incluirá una cabecera Retry-After con los segundos que debes esperar. Las tareas fallidas (tras todos los reintentos) reembolsan automáticamente los créditos descontados.