yoh
เอกสารอ้างอิง API

เอกสารอ้างอิง API

สร้างวิดีโอ AI ผ่านโปรแกรม ส่งพรอมป์ ตรวจสอบสถานะ แล้วรับ URL วิดีโอโดยตรง — ทั้งหมดผ่าน REST

Base URL: https://yoh.app

ตัวอย่างโค้ดจะใช้ตัวยึดตำแหน่ง sk_live_YOUR_API_KEY จนกว่าคุณจะลงชื่อเข้าใช้

เริ่มต้นใช้งาน

สร้างวิดีโอแรกของคุณด้วยการเรียก API เพียง 3 ครั้ง: สร้าง → ตรวจสอบสถานะ → ดาวน์โหลด

1. สร้างวิดีโอ
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"
}
2. ตรวจสอบสถานะจนกว่าจะเสร็จสิ้น
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
}

เท่านี้ก็เรียบร้อย videoUrl คือ URL ที่เข้าถึงได้แบบสาธารณะ คุณสามารถดาวน์โหลด สตรีม หรือแชร์ได้ทันที

การยืนยันตัวตน

คำขอ API ทั้งหมดต้องแนบ API key ของคุณในส่วนหัว Authorization โดยใช้รูปแบบ Bearer

Authorization: Bearer sk_live_YOUR_API_KEY

API key จะขึ้นต้นด้วย sk_live_ คุณสามารถสร้างและจัดการ key ได้จากAPI Keys page /api-keys

เก็บ API key ของคุณเป็นความลับ อย่าเปิดเผยไว้ในโค้ดฝั่งไคลเอนต์หรือ repository สาธารณะ หาก key ถูกเปิดเผย ให้เพิกถอนทันทีจากหน้า API Keys

ขอบเขตสิทธิ์ & เครดิต

API key แต่ละอันจะได้รับสิทธิ์หนึ่งขอบเขตขึ้นไปที่ควบคุมว่า endpoint ใดที่เข้าถึงได้ การสร้างข้อความเรื่องราวมีค่าใช้จ่าย 55 เครดิตต่องาน ส่วนขอบเขตที่สร้างวิดีโอคลิปหรือการเรนเดอร์สุดท้ายด้วย (รวมถึงขอบเขตเริ่มต้นคือ full) จะคิดค่าใช้จ่ายล่วงหน้าเพิ่มเติม เครดิตจะถูกคืนหากงานล้มเหลวหลังจากลองใหม่ครบทุกครั้งแล้ว

ขอบเขตสิทธิ์ให้สิทธิ์เข้าถึง
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 — the clip edits: POST/PATCH /stories/:id/clips, PATCH/DELETE /stories/:id/clips/:clipId — 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, POST /api/v1/stories/estimate, POST /api/v1/stories/analyze-prompt. Implies series:read and assets:read.
stories:writeThe 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 /api/v1/series/:id/assets
assets:writePOST /api/v1/series/:id/assets, PATCH/DELETE /api/v1/series/:id/assets/:assetId, POST /api/v1/uploads
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.

สร้างวิดีโอ

เพิ่มงานสร้างวิดีโอเข้าคิว

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

ส่งพรอมป์และคืนค่า jobId ทันที วิดีโอจะถูกสร้างแบบอะซิงโครนัส ตรวจสอบสถานะได้ที่ GET /api/v1/stories/jobs/:jobId

เนื้อหาคำขอ

ฟิลด์ประเภทสถานะคำอธิบาย
promptstringจำเป็นคำอธิบายเรื่องราว สูงสุด 5000 ตัวอักษร
aspectRatiostringไม่บังคับหนึ่งใน 16:9, 9:16, 1:1, 21:9 ค่าเริ่มต้น: 16:9
renderVideobooleanไม่บังคับfalse — คืนค่าคลิป AI แรก (เร็ว ~10 นาที) true — เรนเดอร์คลิปทั้งหมดรวมเป็น MP4 เดียวผ่าน Remotion (~12–15 นาที)
scopestringไม่บังคับstory (text only, recommended), assets (+ character/scene/item images), storyboard (+ per-clip images), videos (+ clip videos), render (+ final MP4), or full (all phases). Default: full — which charges video + render credits up front. An unrecognized value falls back to full.
titlestringไม่บังคับชื่อที่แสดงของเรื่องราว
contentTypestringไม่บังคับหมวดหมู่รูปแบบเรื่องราว หนึ่งใน cinematic_story, news_analysis, persona_channel, ad_spot ค่าเริ่มต้น: cinematic_story
localestringไม่บังคับรหัสภาษา เช่น en, zh-TW ค่าเริ่มต้น: en
seriesIdstringไม่บังคับแนบเข้ากับซีรีส์ที่มีอยู่แล้วเพื่อให้ภาพตัวละครมีความสอดคล้องกัน
assetsarrayไม่บังคับThe cast and props for this story — reference assets already in your series library, or attach one-off assets inline. See the table below.
continuityModestringไม่บังคับWith a seriesId: how much of the previous episodes this one carries. episodic_continuous (default) picks up open threads, finale resolves them, one_off_episode keeps series plot out entirely.
isFinalebooleanไม่บังคับImplied by continuityMode: "finale"; you rarely need both.
allowMultiStorybooleanไม่บังคับLet one request split into several episodes when the prompt clearly carries more than one. Default false — N episodes means N charges, so it is opt-in.
seriesContinuityContextobjectไม่บังคับEscape hatch. Supplying it replaces the continuity we assemble from your series (open threads plus the last three episode recaps), so an incomplete object produces a weaker episode than sending nothing. Prefer continuityMode.

assets[] ไม่บังคับ

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.

ฟิลด์ประเภทคำอธิบาย
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. 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 ไม่บังคับ

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, qualityTier, modelStrategy and seed are API-only — the studio expresses the same choices through its quality picker. Two fields are advisory: they are shown to the writer as context but nothing enforces them, and they are marked below.

ฟิลด์ประเภทValuesDefault
durationSecnumber15–300 (total seconds)60
aspectRatiostring16:9 · 9:16 · 1:1 · 21:916:9
resolutionstring480p · 720p · 1080p720p
pacestringslow · normal · fastnormal
structurestringauto · 3_act · 4_act · montageauto
qualityTierstringdraft · balanced · premium (premium selects the advanced model lane)balanced
stylePresetstringpreset slug, or autoauto
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 — muapi/sd2-video-edit, or auto. Omit to keep the existing editing recipe.auto
imageModelstringstoryboard frames + scene/item images — a model id, or auto (GPT Image 2)auto
characterImageModelstringcharacter portraits, sheets and appearance variants — a model id, or auto (Nano Banana 2)auto
modelStrategystringauto · fixed · cost_optimized · quality_first · hero_premiumauto
generationModestringDEPRECATED — ignored. Every clip uses unified reference mode.auto
localestringen · zh-TW · ja · ko · th · id (also accepted top-level)en

contentTypeConfig ไม่บังคับ

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.

contentTypeFields
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 (the advertised product)

Other optional fields

ฟิลด์ประเภทคำอธิบาย
storyQualityConfigobject{ enableStyleAdaptedCharacters?: boolean, directClipImageGeneration?: boolean } — quality-rollout toggles. Omit to use pipeline defaults.
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.

Full control example
curl -X POST $BASE/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"
    }
  }'

การตอบกลับ 202 Accepted

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

Step-by-Step Endpoints

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

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.

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.

Credits: 20 minimum, then ~3 per clip beyond that | Scope: stories:generate
POST/api/v1/stories/:storyId/videos

Generate video for each clip. Skips clips that already have a video URL.

Credits: ~96 per 8s clip (default model; scales with duration) | Scope: stories:generate
POST/api/v1/stories/:storyId/render

Stitch all clip videos + music + captions into a final MP4.

Credits: 10 | Scope: stories:generate

One Clip at a Time

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.

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.

Credits: one image (no pipeline base) | Scope: stories:generate
POST/api/v1/stories/:storyId/clips/:clipId/video

(Re)generate the video for ONE clip. Optional body: videoModel. There is no mode to pick — every clip is produced in the single unified reference mode, where a multi-reference model receives one categorized bundle: the storyboard sketch and clip image, character sheets, item, background and style images, the previous clip's tail as a video reference, and the character's voice sample as an audio reference.

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 job. The right call after changing a clip's description or cast: regenerating only the video would run it against the old image. Optional body: prompt (art direction for the new image).

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. Runs inline and returns the grid URL.

Credits: one storyboard grid | Scope: stories:generate
POST/api/v1/stories/:storyId/clips-all

Draw every missing storyboard image and then generate every missing clip video, as ONE job. Running /storyboard and /videos separately lets the video pass start while images are still missing. Both phases only fill gaps, so re-running after a partial failure is safe.

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. TEXT only — the new clips arrive with no images or video, so drawing them is a separate, separately-priced step.

Credits: one story continuation | 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).

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

Rewrite a clip's existing video from an instruction — video-to-video, so subject, composition and motion carry over. Required body: prompt ("make it night and rainy", "push the camera in slowly"). Optional durationSeconds (4–15); omit it and the edit follows the source's own length. The clip must already have a video, and the result becomes a new selectable version rather than replacing the old one.

Credits: one video edit (flat, independent of duration) | Scope: stories:generate
Re-roll one shot
# Regenerate clip 3's image, then its video — the rest of the story is untouched
curl -X POST $BASE/api/v1/stories/STORY_ID/clips/CLIP_ID/image \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
# Poll jobId until completed

curl -X POST $BASE/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 jobId until completed → the clip's videoUrl is replaced

Editing the Story

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.

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

Edit a clip. Send only what you want to change: title, description, motionDescription, duration, generationDuration, resolution, transitionStyle, sceneId, characterIds, itemIds, dialogue, audioFx.

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.

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

Insert a clip. title and description required; place it with afterClipId or beforeClipId (omit both to append). Following clips shift down automatically. The new clip has no image or video yet.

Credits: 0 | Scope: stories:generate
PATCH/api/v1/stories/:storyId/clips

Reorder. Body: { "clips": [{ "id": "...", "order": 0 }, …] } — send every clip with a contiguous 0-based order.

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

Cut a clip; the remaining orders close up. A story must keep at least one clip.

Credits: 0 | Scope: stories:generate
Rewrite a shot, then re-shoot it
# 1. Rewrite the direction + retime the shot (immediate, free)
curl -X PATCH $BASE/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 $BASE/api/v1/stories/STORY_ID/clips/CLIP_ID/image \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
curl -X POST $BASE/api/v1/stories/STORY_ID/clips/CLIP_ID/video \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
Step-by-step example
# Step 1: Generate story text (~1-2 min)
curl -X POST $BASE/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 $BASE/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 $BASE/api/v1/stories/STORY_ID/videos \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
# Poll until completed

# Step 4: Render final MP4 (~1-2 min)
curl -X POST $BASE/api/v1/stories/STORY_ID/render \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
# Poll until completed → videoUrl

ตรวจสอบสถานะงาน

ดูสถานะงาน

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

ตรวจสอบ endpoint นี้หลังจากเรียก POST /api/v1/stories/generate โดยทั่วไปงานจะใช้เวลา 8–15 นาที ขึ้นอยู่กับความยาวของเรื่องราวและว่าเปิดใช้ renderVideo หรือไม่ แนะนำให้ตรวจสอบทุก 15–30 วินาที

วงจรสถานะของงาน

queuedgenerating_storygenerating_assetsgenerating_videosrendering_videocompletedหรือfailed

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.

ฟิลด์ในการตอบกลับ

ฟิลด์ประเภทคำอธิบาย
jobIdstringรหัสเฉพาะของงาน
statusstringสถานะปัจจุบัน ดูวงจรสถานะด้านบน
progressnumberจำนวนเต็ม 0–100 แสดงเปอร์เซ็นต์ความคืบหน้า
storyIdstring|nullจะถูกตั้งค่าเมื่อการสร้างเรื่องราวเสร็จสมบูรณ์ ใช้เพื่อดึงข้อมูลอาเซ็ท
renderVideobooleanระบุว่ามีการร้องขอ MP4 สุดท้ายแบบรวมคลิปหรือไม่
videoUrlstring|nullURL วิดีโอแบบสมบูรณ์เมื่อสถานะเป็น completed อาจเป็น MP4 แบบรวมคลิป (renderVideo: true) หรือวิดีโอ AI ของคลิปแรก (renderVideo: false)
errorstring|nullข้อความแสดงข้อผิดพลาดที่อ่านเข้าใจง่าย จะถูกตั้งค่าเมื่อเกิดความล้มเหลว — และขณะกำลังลองงานใหม่ด้วย ซึ่งสถานะจะกลับไปเป็น queued เป็น null เสมอเมื่อสำเร็จ
createdAt / startedAt / completedAtstring (ISO)ประทับเวลา

เรื่องราว

รายการเรื่องราว

GET/api/v1/storiesscope: stories:read

คืนค่ารายการเรื่องราวของคุณแบบแบ่งหน้า

พารามิเตอร์การค้นหา

พารามิเตอร์ค่าเริ่มต้นคำอธิบาย
page1หมายเลขหน้า (≥ 1)
limit20ผลลัพธ์ต่อหน้า (1–100) Capped at 100.
statusกรองตามสถานะของเรื่องราว

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

ดูเรื่องราวหนึ่งรายการ

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

คืนค่าข้อมูลเมตาแบบสมบูรณ์สำหรับเรื่องราวหนึ่งรายการ รวมถึงจำนวนตัวละคร ฉาก ไอเทม และคลิป

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

ภาพรวมอาเซ็ท

หลังจากงานเสร็จสมบูรณ์ คุณสามารถดึงอาเซ็ททั้งหมดที่สร้างสำหรับเรื่องราวได้โดยใช้ storyId ที่ได้จากงานนั้น endpoint อาเซ็ททั้งหมดต้องใช้ขอบเขตสิทธิ์ assets:read

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

ตัวละคร — ชื่อ คำอธิบาย บทบาท imageUrl ข้อมูลเสียง

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

ฉาก — คำอธิบาย ช่วงเวลาของวัน สภาพอากาศ แสง imageUrl

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

อุปกรณ์ประกอบฉาก / ไอเทม — คำอธิบาย ความสำคัญ imageUrl

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

คลิป — บรรยาย ความยาว videoUrl imageUrl audioUrl

ตัวละคร

รายการตัวละคร

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

คลิป & วิดีโอ

รายการคลิป

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

แต่ละคลิปคือส่วนของฉากที่มีวิดีโอสร้างโดย AI ภาพนิ่ง และเสียงเป็นของตัวเอง ฟิลด์ URL ทั้งหมดเป็น URL แบบสมบูรณ์และเข้าถึงได้แบบสาธารณะ

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

เมื่อ renderVideo เป็น false videoUrl ของงานจะชี้ไปยังวิดีโอของคลิปแรก ใช้ endpoint นี้เพื่อรับ URL วิดีโอของคลิปแต่ละรายการ

คลังอาเซ็ทของซีรีส์

อัปโหลดครั้งเดียว ใช้ซ้ำได้ตลอด

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

เป็นคลังเดียวกับที่แผงอาเซ็ทในสตูดิโอเขียนลงไป อัปโหลดตัวละคร ฉากหลัง พร็อพ หรือภาพอ้างอิงสไตล์ แล้วเก็บ id ที่ได้กลับมา จากนั้นตอนสร้างงานให้ส่ง id นั้นเป็น seriesAssetId ในอาร์เรย์ assets โดยไม่ต้องสร้างลิงก์รูปใหม่ทุกครั้ง รูปที่คุณส่งมาจะถูกนำไปโฮสต์บนพื้นที่จัดเก็บของเรา ดังนั้น id จะยังใช้งานได้แม้ URL ต้นทางของคุณจะหมดอายุ

ฟิลด์ประเภทคำอธิบาย
typestringcharacter, scene, item หรือ reference
namestringชื่อที่แสดง และเป็นคีย์ที่ใช้จับคู่ในการสร้างงานครั้งถัดไป
imageUrlstringURL สาธารณะของรูปภาพ หรือจะส่ง imageBase64 แทนก็ได้
itemTypestringเฉพาะพร็อพ: prop, product หรือ logo โดย logo จะถูกวางแบบแบน ไม่ใส่มิติ 3 มิติ
isHeroProductbooleanเฉพาะพร็อพ: สินค้าหลักของโฆษณานี้ จะปรากฏในทุกคลิป
isPrimaryBackgroundbooleanเฉพาะฉาก: สตูดิโอ/ฉากที่ใช้ซ้ำในทุกช็อตของผู้ประกาศ
  • ส่งอาเซ็ทเดียว หรือใช้อาร์เรย์ "assets" เพื่อบันทึกพร้อมกันได้สูงสุด 100 รายการ
  • ค่าเริ่มต้นจะรวมรายการเมื่อชื่อตรงกันทุกตัวอักษร การอัปโหลดแคตตาล็อกเดิมซ้ำจึงไม่สร้างรายการซ้ำ ใช้ ?mergeStrategy=fuzzy เพื่อจับคู่แบบยืดหยุ่นต่อการเปลี่ยนชื่อ หรือ insert เพื่อสร้างรายการใหม่เสมอ
  • ตั้ง isHeroProduct บนพร็อพ (ad_spot) หรือ isPrimaryBackground บนฉาก (news_analysis / persona_channel) เพื่อปักหมุด ระบบจะแปลงค่าให้อัตโนมัติตอนสร้างงาน คุณไม่ต้องเขียน heroItemId เอง
ขอบเขตสิทธิ์ให้สิทธิ์เข้าถึง
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
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

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

1 — create the show (once)
curl -X POST https://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 — upload the logo (once) and keep the id
curl -X POST https://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 — generate an ad that features it (every time)
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 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.

อาเซ็ทของซีรีส์

ลงทะเบียนภาพตัวละครสำหรับซีรีส์

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

POST /api/v1/series-assets/ensure-character-image ยังใช้งานได้และไม่มีการเปลี่ยนแปลง แต่รองรับเฉพาะตัวละคร แนะนำให้ใช้เอนด์พอยต์นี้แทน

อัปโหลดและบันทึกภาพอ้างอิงตัวละครสำหรับซีรีส์ เพื่อให้ AI ใช้ใบหน้า/ลุคที่สอดคล้องกันในทุกเรื่องราวของซีรีส์นั้น การดำเนินการนี้เป็นแบบ idempotent — เรียกซ้ำหลายครั้งด้วยชื่อตัวละครเดียวกันได้อย่างปลอดภัย

ฟิลด์ประเภทคำอธิบาย
seriesIdstringรหัสของซีรีส์ที่จะแนบตัวละครเข้าไป
characterNamestringชื่อของตัวละคร (ใช้เป็นคีย์เฉพาะภายในซีรีส์)
imageUrlstringURL ที่เข้าถึงได้แบบสาธารณะของภาพอ้างอิงตัวละคร

เผยแพร่ลงโซเชียล

ส่งวิดีโอที่เสร็จแล้วไปยังบัญชีที่เชื่อมต่อไว้ได้ทันที ระบบอัตโนมัติจึงขับเคลื่อน สร้าง → เรนเดอร์ → เผยแพร่ ได้ด้วยข้อมูลรับรองชุดเดียว โปรดเชื่อมต่อบัญชีในแอปก่อน เอนด์พอยต์นี้จะเผยแพร่ไปยังบัญชีเหล่านั้น

เผยแพร่วิดีโอ

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

คืนค่า 202 พร้อม { postId, status: "publishing" } และอัปโหลดอยู่เบื้องหลัง — ให้ poll ที่เอนด์พอยต์สถานะด้านล่าง หากต้องการให้รอจนอัปโหลดเสร็จแทน ให้ส่ง wait: true (อาจใช้เวลาหลายนาที)

ฟิลด์ประเภทสถานะคำอธิบาย
socialAccountIdstringจำเป็นบัญชีที่เชื่อมต่อไว้ซึ่งเป็นของผู้ใช้เจ้าของคีย์นี้
storyIdstringจำเป็นสตอรี่ที่เป็นของผู้ใช้เจ้าของคีย์นี้
videoUrlstringจำเป็นURL ของ MP4 แบบสาธารณะ — คือ videoUrl จากงานที่เสร็จแล้ว
platformstringจำเป็นfacebook · instagram · tiktok · threads · youtube
captionstringไม่บังคับแคปชันของโพสต์
youtubeobjectไม่บังคับ{ title, privacyStatus, categoryId, tags } — สำหรับ YouTube เท่านั้น
durationSecnumberไม่บังคับใช้ตรวจสอบความเข้ากันได้กับแพลตฟอร์มก่อนเผยแพร่
aspectRatiostringไม่บังคับใช้ตรวจสอบความเข้ากันได้กับแพลตฟอร์มก่อนเผยแพร่
fileSizeBytesnumberไม่บังคับใช้ตรวจสอบความเข้ากันได้กับแพลตฟอร์มก่อนเผยแพร่
waitbooleanไม่บังคับรอจนกว่าการอัปโหลดจะเสร็จ ค่าเริ่มต้นคือ false
curl -X POST $BASE/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" }
  }'

ตรวจสอบสถานะการเผยแพร่

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

วงจรสถานะ: publishingpublished | failed.

บัญชีที่จัดการให้

สำหรับแพลตฟอร์มที่ให้บริการลูกค้าจำนวนมาก แทนที่จะใช้คีย์เดียวและกองเครดิตเดียวร่วมกันทั้งหมด — ซึ่งลูกค้ารายที่ใช้หนักจะดูดยอดคงเหลือของคนอื่นจนหมด และไม่มีใครบอกได้ว่าใครใช้ไปเท่าไร — ให้เปิดบัญชีแยกต่อลูกค้าหนึ่งราย แล้วเติมเครดิตให้แต่ละบัญชีจากยอดคงเหลือของคุณเอง ลูกค้าของคุณไม่ต้องสมัครที่นี่และไม่ต้องจ่ายเงินให้เรา ความสัมพันธ์ทางการค้ายังเป็นของคุณ

เปิดบัญชี

POST/api/v1/accountsscope: accounts:manage

externalId คือรหัสของคุณเองที่ใช้แทนลูกค้ารายนั้น การเรียกซ้ำด้วยค่าเดิมจะคืนบัญชีเดิมแทนที่จะสร้างบัญชีที่สอง การลองใหม่จึงปลอดภัย API key จะถูกส่งกลับเพียงครั้งเดียว ตอนสร้างครั้งแรก — เราเก็บไว้เป็นแฮชและไม่สามารถแสดงให้ดูอีกได้ จึงควรบันทึกไว้ทันทีที่ได้รับ บัญชีใหม่จะเริ่มต้นที่ยอดคงเหลือศูนย์

ฟิลด์ประเภทคำอธิบาย
externalIdstringจำเป็น รหัสของคุณสำหรับลูกค้ารายนี้ เช่น tenant id ต้องไม่ซ้ำภายในแพลตฟอร์มเดียวกัน
namestringป้ายกำกับ (ไม่บังคับ) สำหรับใช้ทำรายงานของคุณเอง

เติมเครดิตให้บัญชี

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

ย้ายเครดิตจากยอดคงเหลือ "ของคุณ" ไปยังบัญชีนั้น — เป็นการโอน ไม่ใช่การให้เปล่า คุณจึงแจกได้เท่าที่ซื้อไว้จริงเท่านั้น จะคืน 402 เมื่อยอดคงเหลือของคุณไม่พอ ส่วน :accountId รับได้ทั้งรหัสบัญชีของเราและ externalId ของคุณ

ฟิลด์ประเภทคำอธิบาย
amountintegerจำเป็น จำนวนเครดิตที่จะย้าย ต้องเป็นจำนวนบวก
idempotencyKeystringใส่รหัสใบแจ้งหนี้หรือรอบการสมัครสมาชิกของคุณ การเรียกซ้ำหรือส่งซ้ำจะคืนรายการโอนเดิมแทนการเติมเครดิตซ้ำสองครั้ง
descriptionstringหมายเหตุ (ไม่บังคับ) แสดงทั้งสองฝั่งของการโอนในประวัติเครดิต

GET /api/v1/accounts จะแสดงรายการบัญชีของคุณพร้อมยอดคงเหลือและยอดใช้จ่ายสะสมของแต่ละบัญชี ส่วน GET /api/v1/accounts/:accountId/credits ใช้อ่านทีละบัญชี คีย์ของบัญชีที่จัดการให้จะมีเฉพาะขอบเขตการสร้างเท่านั้น — ไม่สามารถเปิดหรือเติมเครดิตให้บัญชีใดได้เลย

Claude Code (MCP)

Yoh มีเซิร์ฟเวอร์ MCP ให้ เพื่อให้เอเจนต์เขียนโค้ด AI (Claude Code, Claude Desktop หรือไคลเอนต์ MCP ใดก็ได้) เปิดและแก้ไขสตอรี่ของคุณได้โดยตรง ทั้งเนื้อหาคลิปและบทพูด การเลื่อนและตัดบนไทม์ไลน์ เลเยอร์อิสระ และการสร้างใหม่กับการเรนเดอร์เมื่อคุณอนุมัติอย่างชัดเจนทุกครั้ง มันทำงานตามกฎเดียวกับตัวแก้ไขในเบราว์เซอร์ การแก้ไขจากเทอร์มินัลจึงลงตำแหน่งเดียวกับที่การลากจะได้

เชื่อมต่อ

สร้าง API key ที่ /api-keys (ถ้าต้องการแค่แก้ไขโดยไม่ใช้เครดิต ขอบเขต stories:read และ stories:write ก็เพียงพอ) จากนั้น:

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

หรือคอมมิตไฟล์ตั้งค่าโปรเจกต์ เพื่อให้ทั้งทีมได้การเชื่อมต่อทันทีที่เปิดรีโป โดยแต่ละคนใส่คีย์ของตัวเองผ่าน env:

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

จากนั้นก็คุยกับ Claude Code ได้เลย: "กระชับจังหวะของตอนที่ 3 — ตัดช่วงเงียบออก เลื่อนคำบรรยายให้ตรงจังหวะ แล้วเรนเดอร์ใหม่" รัน /mcp ใน Claude Code เพื่อตรวจสอบการเชื่อมต่อ

เอเจนต์ทำอะไรได้บ้าง

เครื่องมือขอบเขตตัวอย่าง
อ่านstories:readlist_stories · get_story · get_timeline · get_clip · get_job · get_credits · get_asset_images · get_soundtrack · list_overlays · get_revoice · list_style_presets · list_voices · list_renders · list_audio_assetsget_clip จะแนบภาพสตอรี่บอร์ดมาด้วย และเมื่อใส่ withFrames จะแนบเฟรมจริงที่สุ่มจากวิดีโอที่สร้างแล้ว ซึ่งเป็นทางเดียวที่เอเจนต์จะเห็นว่าคลิปหน้าตาเป็นอย่างไรจริง ๆ ส่วน get_asset_images ทำแบบเดียวกันกับตัวละคร ฉาก และพร็อพ
สร้างstories:generateestimate_story · create_storyเขียนสตอรี่ใหม่ทั้งเรื่องจากพรอมต์ estimate_story ใช้ฟรีและตีราคาทั้งแผนล่วงหน้าได้ ซึ่งใบเสนอราคารายการย่อยทำไม่ได้ ควรเริ่มที่ scope="story" (ข้อความอย่างเดียว) แทน "full" ที่จะคิดค่าวิดีโอทันที
ซีรีส์series:read / series:writelist_series · get_series · create_series · update_series · list_series_assets · add_series_asset · delete_series_assetชั้นที่อยู่เหนือสตอรี่เดียว: ตัวละคร ฉาก และสไตล์ที่ใช้ร่วมกันข้ามตอน ผูกสตอรี่ใหม่ด้วย seriesId ของ create_story เพื่อสืบทอดทั้งหมด
คลิปและบทพูดstories:writeupdate_clip · update_clips · set_all_clips · reorder_clips · add_clip · delete_clip · duplicate_clip · reorder_clip · set_clip_duration · select_clip_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_settingsบทพูดที่ยาวเกินคลิปจะขยายความยาวของคลิปให้อัตโนมัติ การแก้ข้อความไม่เคยเปลี่ยนสื่อที่สร้างไปแล้ว update_clips แก้ไขหลายคลิปพร้อมกันโดยแต่ละคลิปมีค่าของตัวเอง ส่วน set_all_clips ใส่ค่าเดียวกันให้ทุกคลิปและปฏิเสธข้อความ เพราะคำบรรยายเดียวกันทั้งตอนจะทำให้เรื่องแบนราบ ทั้งคู่จะปฏิเสธทั้งชุดหากมีรายการใดผิดพลาด reorder_clips ตั้งลำดับทั้งเรื่องได้ในครั้งเดียว
ไทม์ไลน์stories:writemove_clip · resize_clip · trim_clip · reset_clip_timing · close_gaps · move_caption · hide_clip_video · pin_dialogue_read · export_clip_to_freeform · export_caption_to_freeform · remove_soundtrackการเลื่อนคลิปจะดันคลิปถัดไปทั้งหมด ส่วนการปรับความยาวยึดจุดเริ่ม ดังนั้นการลากด้านหน้าคือการตัดหัว รายการของสตอรี่ลบหรือแยกไม่ได้ — ให้ซ่อนไว้ หรือส่งออกเป็นเลเยอร์อิสระ
เลเยอร์stories:writeadd_overlay · update_overlay · move_overlay · resize_overlay · split_overlay · duplicate_overlay · delete_overlay · delete_freeform_row · upload_mediaเลเยอร์สองอันบนแถวเดียวกันซ้อนทับกันไม่ได้ ตำแหน่งที่ชนกันจะถูกปฏิเสธพร้อมบอกแถวที่ว่าง สื่อต้องมี https ที่โฮสต์ไว้แล้ว
สไตล์stories:writelist_style_presets · apply_overlay_preset · set_caption_styleพรีเซ็ตข้อความ เทมเพลตคำบรรยาย และแอนิเมชันเข้า-ออกราว 32 แบบของตัวแก้ไขเอง เพื่อให้เอเจนต์ใช้สิ่งที่ผลิตภัณฑ์มีอยู่จริงแทนการคิด CSS ขึ้นเอง สไตล์คำบรรยายกำหนดแยกทีละบรรทัดบทพูด
ตัวละครและฉากstories:writeadd_character · update_character · delete_character · add_scene · update_scene · delete_scene · add_item · update_item · delete_itemการลบตัวละครจะซ่อมทุกการอ้างอิง — ทั้งการจัดตัวละครในคลิปและบทพูดของมัน ซึ่งจะย้ายไปหาตัวละครชื่อเดียวกันที่เหลืออยู่ หรือไปที่ผู้บรรยาย
กำหนดเสียงstories:writelist_voices · set_character_voiceเสียงสำเร็จรูปที่กรองตามภาษาของสตอรี่ รวมกับเสียงโคลนที่พร้อมใช้ในบัญชี การกำหนดเสียงไม่มีค่าใช้จ่าย และบทพูดที่พากย์ไปแล้วจะคงเสียงเดิมที่ใช้ตอนสร้าง
พากย์ใหม่stories:writeget_revoice · set_revoice_enabled · select_revoice_take · undo_revoiceการสลับระหว่างเสียงอ่านใหม่กับการแสดงต้นฉบับไม่มีค่าใช้จ่ายและย้อนกลับได้ เก็บไว้ทั้งสองเทค
การสร้างstories:generateregenerate_clip · regenerate_clip_image · edit_clip_image · enhance_clip_image · generate_clip_video · edit_clip_video · 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 · render_storyแต่ละคู่คือ 'แก้' กับ 'วาดใหม่': edit_clip_image เขียนทับเฟรมปัจจุบันตามคำสั่ง ส่วน regenerate_clip_image วาดขึ้นใหม่ทั้งภาพ; edit_clip_video เป็นวิดีโอสู่วิดีโอที่รักษาตัวแบบ องค์ประกอบ และการเคลื่อนไหวไว้ ส่วน generate_clip_video สร้างใหม่จากภาพสตอรี่บอร์ด enhance_clip_image ดึงใบหน้าให้กลับมาเหมือนตามชีตตัวละคร regenerate_clip ทำภาพแล้วต่อด้วยวิดีโอในงานเดียว เหมาะที่สุดหลังแก้คำบรรยายหรือตัวละครของคลิป generate_clip_grid_reference วาดชีตความต่อเนื่องโดยใช้ช่องที่มาจากจังหวะเวลาของคลิปเอง ไม่ใช่ชุดเหตุการณ์ทั่วไปที่ตายตัว

การใช้เครดิตมีสองด่าน เครื่องมือที่มี ★ จะไม่ใช้เครดิตในการเรียกครั้งแรกเด็ดขาด มันจะคืนใบเสนอราคาเครดิตพร้อม confirmToken ที่ลงลายเซ็น เอเจนต์ต้องแสดงใบเสนอราคาให้คุณดู และดำเนินการต่อเมื่อคุณอนุมัติอย่างชัดเจนเท่านั้น นอกจากนี้คีย์ที่ไม่มี stories:generate ก็ไม่สามารถใช้เครดิตได้เลยโดยโครงสร้าง หากต้องการเอเจนต์ที่อ่านและแก้ไขอย่างเดียว ให้สร้างคีย์ที่มีแค่ stories:write

มันคงความปลอดภัยและถูกต้องอย่างไร

  • เซิร์ฟเวอร์บังคับใช้กฎของตัวแก้ไข (การเลื่อนแบบสไลด์ ขอบเขตการตัด เลนคำบรรยาย แถวเลเยอร์) และเมื่อผิดกฎจะบอกช่วงที่อนุญาต แทนที่จะปรับค่าให้เงียบ ๆ
  • เอเจนต์อ่านกฎทั้งหมดจากรีซอร์ส yoh://guide/timeline และพรอมต์ /mcp__yoh__edit-story ให้ขั้นตอนการทำงานที่ปลอดภัย
  • การสร้างเป็นแบบอะซิงโครนัส เครื่องมือจะคืน jobId แล้วเอเจนต์จะ poll ด้วย get_job เหมือน REST API
  • ตั้งค่า timeout ให้ยาวใน .mcp.json (เช่น 900000) เพื่อไม่ให้การเรนเดอร์หมดเวลาฝั่งไคลเอนต์

ตรวจสอบการเชื่อมต่อ

รีโปมีสโมกเทสต์มาให้ มันสื่อสารด้วยโปรโตคอลจริง ทดสอบกฎต่าง ๆ และพิสูจน์ว่าด่านการใช้เครดิตทำงานจริง โดยไม่ใช้เครดิตเลย และการเขียนเพียงอย่างเดียวที่ทำก็จะถูกย้อนกลับ:

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

จัดการ API Key

endpoint การจัดการ API key ใช้ session authentication (คุกกี้ของผู้ใช้ที่ล็อกอินอยู่) ไม่ใช่ API key ใช้ API Keys page สำหรับขั้นตอนการทำงานผ่านเบราว์เซอร์ หรือเรียก endpoint เหล่านี้ผ่านโปรแกรมจากแบ็กเอนด์ของคุณด้วยเซสชันที่ถูกต้อง /api-keys

รายการ API key

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

สร้าง API key

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

เพิกถอน API key

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

ข้อผิดพลาด

ข้อผิดพลาดทั้งหมดจะคืนค่าในรูปแบบ JSON ที่สอดคล้องกัน โดยมีอ็อบเจ็กต์ error ที่มี code ที่เครื่องอ่านได้

{ "error": { "code": "insufficient_credits", "message": "...", "status": 402 } }
HTTPcodeความหมาย
401unauthorizedไม่มี API key หรือ API key ไม่ถูกต้อง
403forbiddenAPI key ไม่มีขอบเขตสิทธิ์ที่จำเป็น
404not_foundไม่พบทรัพยากรที่ร้องขอ
400bad_requestเนื้อหาคำขอหรือพารามิเตอร์ไม่ถูกต้อง
402insufficient_creditsเครดิตไม่เพียงพอสำหรับดำเนินการนี้
429rate_limitedคำขอมากเกินไป ตรวจสอบส่วนหัว Retry-After
500internal_errorเกิดข้อผิดพลาดของเซิร์ฟเวอร์ที่ไม่คาดคิด

ขีดจำกัดอัตราการเรียก: endpoint การสร้างอนุญาต 5 คำขอต่อนาทีต่อ API key การอ่านข้อมูลและการแก้ไขคลิป (ซึ่งไม่ใช้เครดิต) อนุญาต 60 คำขอ เมื่อคุณถึงขีดจำกัด การตอบกลับจะมีส่วนหัว Retry-After ระบุจำนวนวินาทีที่ต้องรอ งานที่ล้มเหลว (หลังจากลองใหม่ครบทุกครั้ง) จะคืนเครดิตที่ถูกหักไปโดยอัตโนมัติ