Referência da API

Referência da API

Gere vídeos com IA de forma programática. Submeta um prompt, consulte o estado até à conclusão e receba um URL direto do vídeo, tudo via REST.

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

Os exemplos de código utilizarão o marcador sk_live_YOUR_API_KEY até iniciar sessão.

Início rápido

Crie o seu primeiro vídeo em três chamadas de API: gerar → consultar → descarregar.

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.

Verifique a sua chave e o saldo (sem custos)
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. Gerar um 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.

Resposta — trabalho em fila de espera
{
  "jobId": "cmnk8wu2q0001q3vpmqv93nmy",
  "status": "queued",
  "pollUrl": "/api/v1/stories/jobs/cmnk8wu2q0001q3vpmqv93nmy"
}
2. Consultar até concluir
curl https://www.yoh.app/api/v1/stories/jobs/cmnk8wu2q0001q3vpmqv93nmy \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
Resposta — trabalho concluído
{
  "status": "completed", "progress": 100,
  "storyId": "cmnk8xxnx000004lbd7siief7",
  "videoUrl": "https://cdn.example.com/video/clip.mp4",
  "error": null
}

Já está. O videoUrl é um URL acessível publicamente que pode descarregar, transmitir ou partilhar imediatamente.

Autenticação

Todos os pedidos de API devem incluir a sua chave de API no cabeçalho Authorization com o esquema Bearer.

Authorization: Bearer sk_live_YOUR_API_KEY

As chaves de API começam com sk_live_. Pode criar e gerir chaves a partir da API Keys page. /api-keys

Mantenha as suas chaves de API em segredo. Não as exponha em código do lado do cliente ou em repositórios públicos. Se uma chave for comprometida, revogue-a imediatamente na página de Chaves de API.

Âmbitos e créditos

Cada chave de API recebe um ou mais âmbitos que controlam a que endpoints pode aceder. A geração do texto da história custa 55 créditos por trabalho; os âmbitos que também produzem vídeos de clipes ou uma renderização final (incluindo o âmbito predefinido, full) cobram esses valores antecipadamente, além daquele custo. Os créditos são reembolsados se o trabalho falhar após todas as tentativas.

ÂmbitoConcede acesso 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.

Gerar um vídeo

Colocar um trabalho de geração de vídeo em fila

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

Submete um prompt e devolve um jobId imediatamente. O vídeo é gerado de forma assíncrona. Consulte GET /api/v1/stories/jobs/:jobId para acompanhar o progresso.

Corpo do pedido

CampoTipoEstadoDescrição
promptstringobrigatórioDescrição da história, máximo de 5000 caracteres.
aspectRatiostringopcionalUm de 16:9, 9:16, 1:1, 21:9. Predefinição: 16:9.
renderVideobooleanopcionalfalse — devolve o primeiro clipe de IA (rápido, ~10 min). true — renderiza todos os clipes num MP4 composto via 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.
titlestringopcionalNome de apresentação da história.
contentTypestringopcionalCategoria de formato da história. Um de cinematic_story, news_analysis, persona_channel, ad_spot. Predefinição: cinematic_story.
localestringopcionalCódigo de idioma, p. ex.: en, zh-TW. Predefinição: en.
seriesIdstringopcionalAssociar a uma série existente para manter a consistência visual das personagens.
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.

CampoTipoDescrição
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.

CampoTipoValoresPredefinição
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.

Outros campos opcionais

CampoTipoDescrição
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.

Exemplo de controlo 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"
    }
  }'

Resposta 202 Accepted

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

Endpoints passo a passo

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

Um clipe de 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)
Gerar de novo um plano
# 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 a história

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
Reescrever um plano e depois voltar a filmar
# 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"
Exemplo passo a passo
# 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 estado do trabalho

Obter estado do trabalho

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

Consulte este endpoint depois de chamar POST /api/v1/stories/generate. O trabalho demora normalmente 8–15 minutos, consoante a duração da história e se renderVideo está ativado. Recomendamos consultar a cada 15–30 segundos.

Ciclo de vida do estado do trabalho

queuedgenerating_storygenerating_assetsgenerating_videosrendering_videocompletedoufailed

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 resposta

CampoTipoDescrição
jobIdstringIdentificador único do trabalho.
statusstringEstado atual. Consulte o ciclo de vida acima.
progressnumberInteiro de 0–100 que indica a percentagem de conclusão.
storyIdstring|nullDefinido assim que a geração da história termina. Utilize-o para obter os elementos.
renderVideobooleanSe foi solicitada uma composição final em MP4.
videoUrlstring|nullURL absoluto do vídeo quando o estado é completed. Pode ser um MP4 composto (renderVideo: true) ou o vídeo de IA do primeiro clipe (renderVideo: false).
errorstring|nullMensagem de erro legível. Definida em caso de falha e também enquanto o trabalho volta a ser tentado, caso em que o estado regressa a queued. Sempre null em caso de sucesso.
waitingFor / messagestring (optional)Presente enquanto um trabalho aguarda o resultado de um fornecedor (o estado permanece queued, o erro é null): o valor é "provider" e a mensagem indica o que está a aguardar. Continue a consultar.
executionobject (optional)A execução do Workflow que detém o trabalho: { version, runId, phase, heartbeatAt, status }. Acompanhe em GET /api/runs/:runId (ou no seu /stream). Ausente em trabalhos mais antigos.
createdAt / startedAt / completedAtstring (ISO)Carimbos de data/hora.
runId / runUrlstring (202 only)Na resposta 202 de cada início de trabalho (não nesta consulta): a execução iniciada para o trabalho e o seu URL de estado. De utilização opcional; consultar este endpoint é suficiente.

Histórias

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 histórias

GET/api/v1/storiesscope: stories:read

Devolve uma lista paginada das suas histórias.

Parâmetros de consulta

ParâmetroPredefiniçãoDescrição
page1Número da página (≥ 1).
limit20Resultados por página (1–100). Capped at 100.
status—Filtrar pelo estado da história.

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

Obter uma história

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

Devolve os metadados completos de uma única história, incluindo a contagem das suas personagens, cenas, objetos e clipes.

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

Visão geral dos elementos

Após a conclusão de um trabalho, pode obter todos os elementos gerados para a história com o storyId devolvido pelo trabalho. Todos os endpoints de elementos requerem o âmbito assets:read.

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

Personagens — nome, descrição, papel, imageUrl, informações de voz

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

Cenas — descrição, timeOfDay, weather, lighting, imageUrl

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

Adereços / objetos — descrição, importância, imageUrl

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

Clipes — narração, duração, videoUrl, imageUrl, audioUrl

Personagens

Listar personagens

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

Clipes e vídeos

Listar clipes

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

Cada clipe é um segmento de cena com o seu próprio vídeo, imagem estática e áudio gerados por IA. Todos os campos de URL são absolutos e acessíveis publicamente.

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

Quando renderVideo é false, o videoUrl do trabalho aponta para o vídeo do primeiro clipe. Utilize este endpoint para obter o URL do vídeo de cada clipe.

Biblioteca de elementos da série

Carregue um elemento uma vez, utilize-o para sempre

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

A mesma biblioteca em que o painel de elementos do estúdio escreve. Carregue uma personagem, fundo, objeto ou referência de estilo, guarde o ID devolvido e use-o no momento da geração, passando esse ID como seriesAssetId na matriz assets; não precisa de criar um novo link de imagem para cada vídeo. As imagens que envia são realojadas no nosso armazenamento, pelo que o ID continua a funcionar depois de o seu URL de carregamento expirar.

CampoTipoDescrição
typestringcharacter, scene, item ou reference.
namestringNome de apresentação. É também a chave com que as gerações posteriores fazem a correspondência.
imageUrlstringURL público da imagem. Alternativamente, envie imageBase64.
itemTypestringApenas objetos: prop, product ou logo. Um logótipo é composto em plano e nunca recebe profundidade 3D.
isHeroProductbooleanApenas objetos: o produto de que trata este anúncio. Aparece em todos os clipes.
isPrimaryBackgroundbooleanApenas cenas: o estúdio/cenário reutilizado em todos os planos de pivô.
seriesDuplicatestringApenas personagens, depois de perguntar ao utilizador: "new" adiciona uma segunda personagem com um nome que a série já tem.
  • Envie um elemento ou uma matriz "assets" para criar ou atualizar até 100 de uma só vez.
  • Por predefinição, os elementos são fundidos quando o nome coincide exatamente, pelo que voltar a carregar o mesmo catálogo é idempotente. A exceção é uma personagem cujo nome a série já tem: nesse caso, a resposta é 409 SERIES_NAME_EXISTS com a personagem existente, e nada é guardado. Pergunte ao utilizador e, em seguida, envie seriesDuplicate "new" para adicionar uma segunda, ou passe ?mergeStrategy=exact para atualizar a existente. ?mergeStrategy=fuzzy tolera mudanças de nome; insert cria sempre um novo elemento.
  • DELETE devolve 409 SERIES_ASSET_IN_USE com os episódios que ainda usam o elemento (stories: id, title, clipCount) e não remove nada. Pergunte ao utilizador e, em seguida, envie confirm: true (no corpo ou ?confirm=true): os episódios mantêm a sua própria cópia e os seus clipes não mudam. Uma personagem com uma contratação em aberto devolve 409 SERIES_ASSET_HIRED (hires) e só pode ser removida quando a contratação terminar.
  • Defina isHeroProduct num objeto (ad_spot) ou isPrimaryBackground numa cena (news_analysis / persona_channel) para o fixar. No momento da geração, a fixação é resolvida automaticamente: nunca precisa de escrever heroItemId à mão.
ÂmbitoConcede acesso 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 — criar a história (uma 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 — carregar o logótipo (uma vez) e guardar o 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 — gerar um anúncio que o inclua (sempre)
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.

Elementos da série

Registar uma imagem de personagem para uma série

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

POST /api/v1/series-assets/ensure-character-image ainda funciona e não foi alterado, mas apenas processa personagens. Prefira este endpoint.

Carrega e guarda uma imagem de referência de personagem para uma série, para que a IA use um rosto/aspeto consistente em todas as histórias dessa série. Esta operação é idempotent: chamá-la várias vezes com o mesmo nome de personagem é seguro.

CampoTipoDescrição
seriesIdstringID da série à qual anexar a personagem.
characterNamestringO nome da personagem (usado como chave única dentro da série).
imageUrlstringURL acessível publicamente da imagem de referência da personagem.

Publicar nas redes sociais

Envie um vídeo terminado diretamente para uma conta ligada, para que uma automatização possa fazer gerar → renderizar → publicar com uma única credencial. Ligue primeiro as contas na app; este endpoint publica nelas.

Publicar um vídeo

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

Devolve 202 com { postId, status: "publishing" } e carrega em segundo plano; consulte o endpoint de estado abaixo. Passe wait: true para, em vez disso, bloquear até o carregamento terminar (pode demorar minutos). O URL do vídeo tem de corresponder a uma renderização bem-sucedida ou ao vídeo original carregado nesta história. Os envios para o YouTube estão limitados a endereços públicos e ao limite de carregamento atual de 512 MB.

CampoTipoEstadoDescrição
socialAccountIdstringobrigatórioUma conta ligada pertencente ao utilizador da sua chave.
storyIdstringobrigatórioUma história pertencente ao utilizador da sua chave.
videoUrlstringobrigatórioURL público do MP4 — o videoUrl de um trabalho concluído.
platformstringobrigatóriofacebook · instagram · tiktok · threads · youtube
captionstringopcionalLegenda da publicação.
youtubeobjectopcional{ title, privacyStatus, categoryId, tags } — apenas YouTube.
durationSecnumberopcionalUtilizado para validação prévia da plataforma.
aspectRatiostringopcionalUtilizado para validação prévia da plataforma.
fileSizeBytesnumberopcionalUtilizado para validação prévia da plataforma.
waitbooleanopcionalBloquear até o carregamento terminar. Predefinição: 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 da publicação

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

Ciclo de vida: publishing → published | failed.

Contas geridas

Para plataformas que servem muitos clientes. Em vez de uma chave e uma reserva de créditos para todos (em que um cliente com muita atividade esgota o saldo dos restantes e ninguém consegue saber quem gastou o quê), crie uma conta por cliente e financie cada uma a partir do seu próprio saldo. Os seus clientes nunca se registam aqui nem nos pagam; a relação comercial continua a ser sua.

Provisionar uma conta

POST/api/v1/accountsscope: accounts:manage

externalId é o seu próprio ID para o cliente. Chamar duas vezes com o mesmo devolve a conta existente em vez de criar uma segunda, pelo que repetir um pedido é seguro. A chave de API é devolvida uma única vez, na primeira criação; guardamos apenas um hash e não a podemos voltar a mostrar, por isso guarde-a quando a receber. Uma nova conta começa com saldo zero.

CampoTipoDescrição
externalIdstringObrigatório. O seu ID para este cliente (por exemplo, um ID de tenant). Único por plataforma.
namestringRótulo opcional, para os seus próprios relatórios.

Substituir a chave de uma conta gerida

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

Se a chave inicial se perdeu, chame este endpoint com a chave da plataforma proprietária e sem corpo no pedido. Use o ID da conta ou o externalId. Revoga atomicamente as chaves anteriores da conta e devolve uma nova apiKey uma única vez (201). Guarde-a em segurança. Repetir esta chamada volta a substituir a chave; as novas tentativas normais de criação de conta nunca substituem uma chave que funciona.

Financiar uma conta

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

Move créditos do seu próprio saldo para o da conta: é uma transferência, não uma oferta, pelo que só pode distribuir o que realmente comprou. Devolve 402 quando o seu saldo não chega. :accountId aceita o nosso ID de conta ou o seu externalId.

CampoTipoDescrição
amountintegerObrigatório. Créditos a mover. Deve ser positivo.
idempotencyKeystringPasse o ID da sua fatura ou do período de subscrição. Assim, uma chamada repetida ou reenviada devolve a transferência original em vez de financiar a conta duas vezes. Repita com a mesma chave, destinatário e montante. Uma repetição não volta a transferir créditos, mesmo que o saldo da plataforma esteja agora esgotado. Alterar o destinatário ou o montante devolve 409. Chaves demasiado longas são rejeitadas em vez de truncadas.
descriptionstringNota opcional, apresentada em ambos os lados da transferência no histórico de créditos.

GET /api/v1/accounts lista as suas contas com o saldo e o gasto total de cada uma; GET /api/v1/accounts/:accountId/credits lê uma conta. A chave própria de uma conta gerida só tem âmbitos de geração; nunca pode criar nem financiar contas.

Claude Code & Codex (MCP)

O Yoh disponibiliza um servidor MCP para que agentes de IA (Claude Code, Codex, Claude Desktop ou qualquer cliente MCP) possam criar e editar histórias na sua conta: escrever uma nova história a partir de uma ideia, adicionar as personagens e gerar o vídeo, ou alterar o conteúdo e o diálogo dos clipes, movimentos e cortes na linha temporal e sobreposições livres. Tudo o que gasta créditos aguarda sempre a sua aprovação explícita. Segue as mesmas regras do editor no navegador, pelo que uma edição feita no terminal fica exatamente onde ficaria com um arrastamento.

Três formas de o utilizar

Escolha a que melhor se adapta às ferramentas do seu agente. A skill do Yoh abaixo ensina as três.

  • MCP + navegador (recomendado no Codex, Claude Code e nas apps Claude) O agente faz o trabalho através destas ferramentas MCP e controla um separador do navegador ao lado (Claude no Chrome, navegador do Codex): após cada passo, abre a ligação que vem na resposta (a tabela do guião após uma nova história, o painel do clipe após um vídeo de clipe, a lista Gerar tudo enquanto uma execução decorre, o editor avançado para a linha temporal e as camadas de texto, o MP4 da renderização) e verifica o resultado na página real. Inicie sessão uma vez nesse navegador com a mesma conta da chave de API. O MCP e a página partilham os mesmos dados: a página do episódio atualiza-se poucos segundos após uma alteração feita por MCP, e o agente recarrega o editor avançado após uma alteração na linha temporal. Cada gasto passa por um único caminho: o orçamento do MCP ou a janela de preço da app.
  • Apenas navegador (sem chave de API) O agente usa as páginas de yoh.app no seu navegador, tal como o faria: Criar → História, a tabela do guião e o painel do clipe, o editor de personagens e o seletor de modelos, Gerar tudo, o editor avançado, versões e exportação. Cada gasto passa pela janela de preço da própria app, e o agente nunca escreve dados de pagamento nem o seu código de início de sessão.
  • Apenas MCP (headless) Para scripts, CI e agências que criam muitas variantes: quote → confirmToken para cada gasto, consulta com get_job / get_run, paragem com cancel_generation, variantes com create_story_versions e generate_all_clips, e transferência do videoUrl de cada renderização terminada. Não é preciso navegador.

Exemplo em todos os modos: um anúncio de 16 segundos em 9:16 em inglês e japonês, escrito numa história com o seu produto, gerado, versionado para japonês e renderizado duas vezes.

Ligar

Recomendado: entre com o Yoh, sem chave de API. Use exatamente este endereço (yoh.app sem www redireciona, e o login confere o endereço). No Claude Code:

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

Depois digite /mcp, escolha yoh, depois Authenticate, entre no Yoh no navegador que abrir com a conta que você quer usar e aprove.

No Codex (CLI, extensão do IDE ou app de desktop do ChatGPT), adicione o servidor e entre; depois defina tool_timeout_sec = 300 em [mcp_servers.yoh] no ~/.codex/config.toml e reinicie o Codex:

codex mcp add yoh --url https://www.yoh.app/api/mcp
codex mcp login yoh
  • App de desktop do Claude ou claude.ai: Customize → Connectors → + Add → Add custom connector, chame de Yoh, cole o endereço, depois Connect e entre. Ative em cada chat por + → Connectors. Nos planos Team ou Enterprise, um proprietário adiciona primeiro.
  • Cursor: adicione yoh com este endereço ao ~/.cursor/mcp.json e entre quando o Cursor pedir.
  • ChatGPT: Plugins → + → Add custom MCP server, chame de Yoh, cole o endereço e entre. Alguns planos exigem que um administrador permita servidores personalizados.

Confira: peça ao seu agente para rodar get_credits; ele deve mostrar seu saldo no Yoh. Você vê e desconecta apps conectados em Configurações → Chaves de API → Apps conectados.

Para o seu agente ver as páginas que ele muda: Claude Code com claude --chrome (a extensão Claude in Chrome), o app de desktop do Claude com o conector Claude in Chrome, ou o Codex no app de desktop do ChatGPT com o navegador dele. O skill diz ao agente quando olhar.

Ou use uma chave de API (scripts, CI, apps sem login)

Crie uma chave de API em /api-keys. Para criar novas histórias e gerar vídeo, inclua stories:generate; para editar sem gastar, bastam stories:read e stories:write. Depois, no Claude Code:

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

Ou faça commit de uma configuração de projeto para que toda a equipa receba a ligação ao abrir o repositório, com cada membro a fornecer a sua própria chave via env:

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

No Codex, adicione o servidor a ~/.codex/config.toml e defina YOH_API_KEY na sua 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

Instalar a skill do Yoh

A skill ensina o seu agente a criar e editar bem uma história Yoh: começar numa série, orçamentar antes de gastar e observar os clipes antes de os julgar. É um ficheiro único, mantido atualizado com o servidor. Execute a linha para o seu agente, ou descarregar 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

Depois, basta pedir: “cria uma história de 60 segundos sobre uma enfermeira do turno da noite que ouve fantasmas” ou “aperta o ritmo do episódio 3: corta os tempos mortos, alinha as legendas com as batidas e volta a renderizar”. Execute /mcp no Claude Code ou no Codex para verificar a ligação.

O que o agente pode fazer

FerramentasÂmbitoExemplos
Lerstories: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 anexa a imagem do storyboard e, com withFrames, fotogramas reais extraídos do vídeo gerado — a única forma de o agente ver o aspeto real de um clipe. get_asset_images faz o mesmo para o elenco, cenários e adereços.
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.
Criar ★stories:generatecreate_storyEscreva uma história totalmente nova a partir de um prompt. estimate_story é gratuito e orçamenta um plano completo antecipadamente, o que as cotações por ação não conseguem fazer. Comece com scope="story" (apenas texto) em vez de "full", que cobra imediatamente pelo vídeo.
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.
Sériesseries: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_mainA camada acima de uma história: as personagens, fundos e estilo partilhados que se mantêm entre episódios. Associe uma nova história com create_story seriesId para os herdar.
Clipes e diálogosstories: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_settingsUm diálogo que ultrapasse o clipe aumenta automaticamente a duração do clipe. As edições de texto nunca alteram multimédia já gerada. update_clips edita muitos clipes de uma vez, cada um com os seus valores; set_all_clips aplica um valor a todos os clipes e recusa texto, porque a mesma descrição em todo o lado deixaria o episódio monótono. Ambos rejeitam o lote inteiro se alguma entrada for inválida. reorder_clips define toda a ordem dos clipes numa única chamada.
Assistente Yohstories:writeask_assistant · apply_assistant · undo_assistantPasse um pedido ao assistente da própria Yoh, o que está por trás do Perguntar à IA da página do guião e do agente de IA do editor. ask_assistant apenas propõe: as edições chegam como alterações e os passos pagos como orçamentos. apply_assistant aplica as alterações que quiser manter; uma paga também precisa do confirmToken do seu orçamento depois de o utilizador concordar. undo_assistant repõe o que uma execução aplicada alterou. Cada turno de ask_assistant custa alguns créditos: o custo real da IA, até um pequeno orçamento por turno.
Linha temporalstories: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_soundtrackMover um clipe faz deslizar todos os clipes seguintes; redimensionar fixa o início, pelo que arrastar o início é um corte no início. Os itens da história não podem ser eliminados nem divididos: oculte-os ou exporte-os para uma sobreposição livre.
Sobreposiçõesstories:writeadd_overlay · update_overlay · move_overlay · resize_overlay · split_overlay · duplicate_overlay · delete_overlay · delete_freeform_row · upload_mediaDuas sobreposições não podem coincidir na mesma linha; uma colocação em conflito é recusada e as linhas livres são listadas. Os ficheiros multimédia precisam de um src https alojado.
Estilostories:writeapply_overlay_preset · set_caption_styleAs predefinições de texto do próprio editor, os modelos de legendas e cerca de 32 animações de entrada/saída, para que o agente aplique o que o produto oferece em vez de inventar CSS. O estilo das legendas é definido por fala.
Personagens e cenáriosstories:writeadd_character · update_character · delete_character · add_scene · update_scene · delete_scene · add_item · update_item · delete_item · keep_character_sheetEliminar uma personagem remove-a de todos os clipes. As suas falas são mantidas sem interlocutor até escolher quem as diz (update_dialogue_line); um papel com um rosto licenciado ou uma contratação pendente não pode ser eliminado.
Casting de vozstories:writeset_character_voiceVozes de catálogo filtradas pelo idioma da história, mais as vozes clonadas prontas da conta. Atribuir vozes é gratuito; as falas já geradas mantêm a voz com que foram criadas.
Regenerar vozstories:writeset_revoice_enabled · select_revoice_take · undo_revoice · fix_audio_syncAlternar entre as novas leituras e a interpretação original é gratuito e reversível; os takes são mantidos em ambos os casos.
Geração ★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 é editar vs. redesenhar: edit_clip_image reescreve o fotograma atual a partir de uma instrução, enquanto regenerate_clip_image desenha um novo; edit_clip_video é de vídeo para vídeo e mantém o sujeito, a composição e o movimento, enquanto generate_clip_video volta a gerar a partir da imagem do storyboard. enhance_clip_image aproxima os rostos das fichas de personagem. regenerate_clip faz imagem e depois vídeo num único trabalho: é a escolha certa depois de alterar a descrição ou o elenco de um clipe. generate_clip_grid_reference desenha uma folha de continuidade cujos painéis vêm dos próprios momentos do clipe, e não de um conjunto fixo 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.

Gastos com dupla verificação Uma ferramenta ★ nunca gasta na primeira chamada: devolve um orçamento em créditos e um confirmToken assinado, e o agente tem de lhe mostrar o orçamento e só avançar com a sua aprovação explícita. Além disso, uma chave sem stories:generate não consegue gastar de todo; crie uma chave só de edição com stories:write se quiser um agente que leia e edite.

Como se mantém seguro e correto

  • O servidor aplica as regras do editor (movimentos de slides, limites de corte, faixas de legendas, linhas de sobreposição) e responde a uma violação com o intervalo permitido em vez de restringir silenciosamente.
  • O agente lê o conjunto completo de regras do recurso yoh://guide/timeline, e o prompt /mcp__yoh__edit-story fornece-lhe um fluxo de trabalho seguro.
  • A geração é assíncrona: a maioria das ferramentas devolve um runId que o agente consulta com get_run; algumas (create_story, render_story, arte de personagem) devolvem um jobId para get_job, tal como na API REST.
  • Defina um tempo de espera (timeout) longo no ficheiro .mcp.json (por exemplo 900000) para que uma renderização não expire no lado do cliente.

Verifique a sua ligação

O repositório inclui um teste de fumo (smoke test). Fala o protocolo real, põe as regras à prova e confirma que o bloqueio de gastos funciona; nunca gasta créditos e a sua única escrita é revertida:

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

Gerir chaves de API

Os endpoints de gestão de chaves de API usam session authentication (cookie do utilizador com sessão iniciada), não uma chave de API. Utilize a API Keys page para um fluxo no navegador ou chame estes endpoints programaticamente a partir do seu backend com uma sessão válida. /api-keys

Listar chaves 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"
  }]
}

Criar uma chave 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"]
}

Revogar uma chave de API

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

Erros

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.

A maioria dos endpoints devolve um objeto error com um code legível por máquina. Endpoints especializados podem devolver um erro em formato de texto ou um código de nível superior; verifique sempre o estado HTTP.

{ "error": { "code": "insufficient_credits", "message": "...", "status": 402 } }
HTTPcódigoSignificado
401unauthorizedChave de API em falta ou inválida.
403forbiddenA chave de API não tem o âmbito necessário.
404not_foundO recurso solicitado não existe.
400bad_requestCorpo do pedido ou parâmetros inválidos.
402insufficient_creditsCréditos insuficientes para realizar a operação.
429rate_limitedDemasiados pedidos. Verifique o cabeçalho Retry-After.
500internal_errorErro inesperado do servidor.

Limites de taxa: Os endpoints de geração permitem 5 pedidos por minuto por chave de API. As leituras e edições de clipes (que não gastam créditos) permitem 60. Quando atingir um limite, a resposta inclui um cabeçalho Retry-After com o número de segundos a aguardar. Trabalhos que falhem (após todas as tentativas) reembolsam automaticamente os créditos que foram deduzidos.