API Reference
Generate AI videos programmatically. Submit a prompt, poll for completion, and receive a direct video URL — all via REST.
Base URL: https://yoh.appCode examples will use the placeholder until you sign in.
Quick Start
Create your first video in three API calls: generate → poll → download.
curl -X POST https://yoh.app/api/v1/stories/generate \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "prompt": "A dragon teaches a young knight how to fly", "aspectRatio": "16:9" }'{
"jobId": "cmnk8wu2q0001q3vpmqv93nmy",
"status": "queued",
"pollUrl": "/api/v1/stories/jobs/cmnk8wu2q0001q3vpmqv93nmy"
}curl https://yoh.app/api/v1/stories/jobs/cmnk8wu2q0001q3vpmqv93nmy \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"{
"status": "completed", "progress": 100,
"storyId": "cmnk8xxnx000004lbd7siief7",
"videoUrl": "https://cdn.example.com/video/clip.mp4",
"error": null
}That's it. The videoUrl is a publicly accessible URL you can download, stream, or share immediately.
Authentication
All API requests must include your API key in the header using the scheme.
Authorization: Bearer sk_live_YOUR_API_KEYAPI keys begin with . You can create and manage keys from the .
Keep your API keys secret. Do not expose them in client-side code or public repositories. If a key is compromised, revoke it immediately from the API Keys page.
Scopes & Credits
Each API key is granted one or more scopes that control which endpoints it can access. Story generation costs credits per job. Credits are refunded if the job fails after all retries.
| Scope | Grants access to |
|---|---|
| stories:generate | POST /api/v1/stories/generate, POST /api/v1/series-assets/ensure-character-image |
| stories:read | GET /api/v1/stories, GET /api/v1/stories/:id, GET /api/v1/stories/jobs/:id |
| assets:read | GET .../characters, .../scenes, .../items, .../clips |
Generate a Video
Enqueue a video generation job
/api/v1/stories/generatescope: stories:generateSubmits a prompt and returns a immediately. The video is generated asynchronously. Poll to track progress.
Request body
| Field | Type | Status | Description |
|---|---|---|---|
prompt | string | required | Story description, max 5000 characters. |
aspectRatio | string | optional | One of , , , . Default: . |
renderVideo | boolean | optional | — returns the first AI clip (fast, ~10 min). — renders all clips into one composed MP4 via Remotion (~12–15 min). |
title | string | optional | Display name for the story. |
contentType | string | optional | Story format category. One of cinematic_story, news_analysis, music_video, persona_channel, youtube_clone, ad_spot. Default: cinematic_story. |
locale | string | optional | Language code, e.g. , . Default: . |
seriesId | string | optional | Attach to an existing series for consistent character visuals. |
Response 202 Accepted
{ "jobId": "cmnk8wu2q0001q3vpmqv93nmy", "status": "queued", "pollUrl": "/api/v1/stories/jobs/..." }Poll Job Status
Get job status
/api/v1/stories/jobs/:jobIdscope: stories:readPoll this endpoint after calling . The job typically takes 8–15 minutes depending on story length and whether is enabled. Recommend polling every .
Job status lifecycle
queuedgenerating_storygenerating_videosrendering_videocompletedorfailedResponse fields
| Field | Type | Description |
|---|---|---|
jobId | string | Unique job identifier. |
status | string | Current status. See lifecycle above. |
progress | number | 0–100 integer indicating completion percentage. |
storyId | string|null | Set once story generation completes. Use to fetch assets. |
renderVideo | boolean | Whether a composed final MP4 was requested. |
videoUrl | string|null | Absolute video URL when status is completed. Either a composed MP4 (renderVideo: true) or the first clip's AI video (renderVideo: false). |
error | string|null | Human-readable error message on failure. Always null on success. |
createdAt / startedAt / completedAt | string (ISO) | Timestamps. |
Stories
List stories
/api/v1/storiesscope: stories:readReturns a paginated list of your stories.
Query parameters
| Param | Default | Description |
|---|---|---|
page | 1 | Page number (≥ 1). |
limit | 20 | Results per page (1–100). |
status | — | Filter by story status. |
Get a story
/api/v1/stories/:storyIdscope: stories:readReturns full metadata for a single story including counts of its characters, scenes, items, and 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"
}Assets overview
After a job completes you can fetch all assets generated for the story using the returned by the job. All asset endpoints require the scope.
GET /api/v1/stories/:id/charactersCharacters — name, description, role, imageUrl, voice info
GET /api/v1/stories/:id/scenesScenes — description, timeOfDay, weather, lighting, imageUrl
GET /api/v1/stories/:id/itemsProps / items — description, significance, imageUrl
GET /api/v1/stories/:id/clipsClips — narration, duration, videoUrl, imageUrl, audioUrl
Characters
List characters
/api/v1/stories/:storyId/charactersscope: assets:read{
"data": [{
"id": "char_abc123", "name": "Chef Axon", "role": "protagonist",
"description": "A robot chef with a warm personality",
"imageUrl": "https://cdn.example.com/characters/chef-axon.jpg",
"voiceName": "Neutral-EN", "order": 0
}]
}Clips & Videos
List clips
/api/v1/stories/:storyId/clipsscope: assets:readEach clip is a scene segment with its own AI-generated video, still image, and audio. All URL fields are absolute and publicly accessible.
{
"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"
}]
}When is false, the job's points to the first clip's video. Use this endpoint to get the video URL for every individual clip.
Series Assets
Register a character image for a series
/api/v1/series-assets/ensure-character-imagescope: stories:generateUploads and persists a character reference image for a series so the AI uses a consistent face/look across all stories in that series. This operation is — calling it multiple times with the same character name is safe.
| Field | Type | Description |
|---|---|---|
seriesId | string | ID of the series to attach the character to. |
characterName | string | The character's name (used as a unique key within the series). |
imageUrl | string | Publicly accessible URL of the character reference image. |
Manage API Keys
API key management endpoints use (logged-in user cookie), not an API key. Use the for a browser-based workflow, or call these endpoints programmatically from your backend with a valid session.
List API keys
/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"
}]
}Create an API key
/api/v1/api-keysscope: session// Response — full key shown once
{
"id": "key_abc123",
"key": "sk_live_...",
"name": "My integration",
"keyPrefix": "sk_live_7WH3",
"scopes": ["stories:generate", "stories:read", "assets:read"]
}Revoke an API key
/api/v1/api-keys/:idscope: session{ "id": "key_abc123", "revoked": true }Errors
All errors return a consistent JSON shape with an object containing a machine-readable .
{ "error": { "code": "insufficient_credits", "message": "...", "status": 402 } }| HTTP | code | Meaning |
|---|---|---|
| 401 | unauthorized | Missing or invalid API key. |
| 403 | forbidden | API key does not have the required scope. |
| 404 | not_found | The requested resource does not exist. |
| 400 | bad_request | Invalid request body or parameters. |
| 402 | insufficient_credits | Not enough credits to perform the operation. |
| 429 | rate_limited | Too many requests. Check the Retry-After header. |
| 500 | internal_error | Unexpected server error. |
Rate limits: Generation endpoints have a stricter rate limit than read endpoints. When you hit a limit, the response includes a header with the number of seconds to wait. Failed jobs (after all retries) automatically refund the 25 credits that were deducted.