Request Format Note When making POST requests, we recommend sending the JSON body directly with the
Content-Type: application/jsonheader. If usingapplication/x-www-form-urlencoded, serialize the parameters as JSON and send them in thepayloadfield. Example:payload={"apiKey":"...","secret":"..."}The
//comments in the JSON examples in this document are for explanation only. Standard JSON does not allow comments, so do not include comments in your actual request body. (Blocks containing comments are marked asjsonc.)
An API for automatically generating and exporting videos without the editing interface. To prevent token exposure, call this API from the backend only.
Automates both project creation and export in a single call. The final result is delivered via POST to the webhookUrl or the Export Webhook configured in your app settings. Projects created this way are automatically deleted one week after export.
| Header | Required | Description |
|---|---|---|
| Authorization | ✓ | Token copied from Account Settings > API Token. Format: Bearer Qjky6mFiMkRn... |
| x-nonce | ✓ | A unique value per request (used to distinguish duplicate requests) |
| Parameter | Required | Type | Description |
|---|---|---|---|
| injector | ✓ | object | Data injection/transformation spec. See Injector section below |
| dictionary | object | TTS/STT text substitution rules. See dictionary section below | |
| baseProjectId | string | projectId of the project to use as a template. Must be in the same space or be public | |
| spaceId | int | Target workspace ID. 0 means the default workspace |
|
| type | string | Export file format. mp4 | jpg |
|
| webhookUrl | string | URL to receive the result upon completion | |
| webhookType | string | Webhook delivery method. form | json |
|
| webhookHeader | string | Custom webhook header. Format: Header-Name: value. Example: x-api-key: abc123 |
curl -X POST "https://videostew.com/api/automations" \
-H "Authorization: Bearer {{bearerToken}}" \
-H "x-nonce: $RANDOM" \
-H "Content-Type: application/json" \
-d '{
"baseProjectId": "88282f69vmqbqr9",
"injector": {
"wizard": {
"source": "url",
"sourceContent": "https://doo39oi115k60.cloudfront.net/wizard-url-example/"
}
}
}'
{
"status": "done",
"result": {
"projectId": "abc123..."
}
}
Checking the result: If you visit
https://videostew.com/v/{projectId}and see the message "Exporting…", rendering is in progress. You can check the completed video at the same URL in about 5 minutes. To receive the completion event in code, specify awebhookUrl.
{
"status": "done",
"projectId": "abc123...",
"size": "1080x1920",
"type": "mp4",
"link": "https://cdn.videostew.com/projects/..."
}
Two data injection methods are available: wizard and data. They are mutually exclusive — only one can be used at a time.
Pass text or a URL and AI will analyze it to automatically create a video. Detailed control is limited, but this is the simplest way to generate a video.
| Parameter | Description |
|---|---|
| source | Source type. text | url |
| sourceContent | Value corresponding to source. Plain text or HTML if text; a URL string if url |
| title | Project title |
| language | Project language. Example: ko, en, ja |
| opts.replace | Slide range for AI to replace. body = replace body slides only, keeping intro/outro from the template as-is (suitable for fixed animations and endings used repeatedly). headbody = AI auto-generates subheadings and replaces subheading + body, keeping intro/outro. all = replace everything including intro/outro |
| opts.visual | Visual source method. stock-mixed/stock-video/stock-image = automatically matches contextually relevant resources from the Videostew stock library (mixed/video only/image only respectively). ai-image = generate images with AI. none = no visuals (suitable when the source URL is already image-rich) |
| opts.autoLineBreak | When set to y, text is split line by line so that line-by-line text animations work as intended. Most templates use this animation by default, so y is recommended |
| opts.visualStyle | Visual style. illust = digital art, photo = photorealistic, flat-vector = vector, editorial-cartoon = editorial cartoon, editorial-illust = newspaper illustration, silhouette = silhouette. Or enter a direct prompt such as "Draw in ink wash painting style" to specify the style you want |
| opts.narrationVoices | Assigns a per-speaker AI voice for conversational content (e.g., podcasts). An object of the form { "neutral": "libraryId", "SpeakerName": "libraryId", ... }. neutral is the default voice applied to slides with no speaker pattern. Speaker names must exactly match the SpeakerName: patterns in the content. For single-voice TTS, the narration settings of the base project are used instead of this field. |
| adjust.duration | Target length. keep = preserve original, 30s | 1m | 3m | 10m | 20m |
| adjust.structure | Structure. keep = original, body = body only, headbody = subheading + content format |
| adjust.style | Script style. keep = preserve original, colloquial = conversational like a podcast, news = formal and objective like a news broadcast, info = clear and concise style optimized for information delivery, insight = in-depth style emphasizing insights and analysis, essay = narrative and reflective essay tone, casual = friendly and relaxed everyday conversational style. Or enter a direct prompt such as "Speak to the viewer in a friendly tone with a touch of humor" to specify the style you want |
If all keys in
adjustare set tokeep, the original text is used as-is without any AI refinement.
{
"wizard": {
"source": "url",
"sourceContent": "https://doo39oi115k60.cloudfront.net/wizard-url-example/",
"language": "ko",
"opts": {
"replace": "all",
"visual": "ai-image",
"visualStyle": "illust",
"autoLineBreak": "y"
},
"adjust": {
"duration": "1m",
"structure": "headbody",
"style": "news"
}
}
}
Conversational (podcast) example - per-speaker AI voice assignment
When content includes speaker patterns such as 진행자: or 게스트:, you can use narrationVoices to assign a different voice to each speaker. Speaker names must exactly match the patterns in the content.
{
"wizard": {
"source": "text",
"sourceContent": "<p>진행자: 안녕하세요, 오늘의 주제는...</p><p>게스트: 네, 반갑습니다...</p>",
"language": "ko",
"opts": {
"replace": "all",
"narrationVoices": {
"neutral": "voice-library-id-default",
"진행자": "voice-library-id-host",
"게스트": "voice-library-id-guest"
}
}
}
}
neutral: The default voice applied to slides that have no speaker pattern (e.g., intro/outro slides).
Directly specifies the project/slide/element structure. Use this when you want to fully control how the video is built. If a match with an existing element is found it is replaced; otherwise a new one is created. Specify only the keys that need to change — unspecified values retain their values from the existing project.
Label ≠ API key: Even if the label you gave an element is
img, the image injection key is alwaysimage. The label (label) is for choosing "which slide/element," while the injection keys (textimagevideo) and the coordinate/style keys are fixed.
data keys
The bgm specified at the top level is applied as the default across the entire slides array. If a value is specified for an individual slide, that value takes precedence.
| Key | Description |
|---|---|
| title | Project title |
| language | Project language. Example: ko, en |
| bgm | Project BGM file URL |
| slides | Array of slides |
data.slides[] keys
| Key | Description |
|---|---|
| label | Slide identification label (matched against the slide labels in the base project). Specifying the same label multiple times duplicates that template slide to increase the slide count. Example: using the main label 3 times creates 3 main slides. Label values can be found in the slide panel at the bottom of the editing screen. |
| narration | Slide narration. Enter text and it will be read in the AI voice configured in the template. To use a self-recorded audio file, enter a public URL |
| sound | Slide sound effect URL |
| bgm | BGM URL applied to this slide only. Takes precedence over the top-level bgm |
| text | Text element content. A plain string is applied to the first text element in the slide. When a slide has multiple text elements and you need to target each precisely, specify an array of { "label": "element-label", "content": "content" } objects (label is based on the element label set in the template). An empty string deletes the element. In this simple mode, only the slide's top-level text/image/video keys are used. If you need element-level control (position, animation, color, etc.), use the elements array in Advanced Settings below. |
| image | Image element libraryId. Multiple values can be specified as an array |
| video | Video element libraryId. Multiple values can be specified as an array |
Note: All fields that accept a URL (
image,video,sound,bgm,narration, etc.) must be publicly accessible addresses in a standard file format supported by Chrome.
Example 1 - Text/image replacement + TTS narration
{
"data": {
"language": "ko",
"bgm": "https://example.com/bgm.mp3", // applied to all slides
"slides": [
{
"label": "title", // matched by slide label in the base project
"text": "오늘의 뉴스 제목", // replaces the template's text element with this content
"image": "https://doo39oi115k60.cloudfront.net/wizard-url-example/01.webp",
"narration": "오늘의 뉴스를 전해드립니다." // a string value generates TTS (follows the narration settings of the base project)
},
{
"label": "body",
"text": "본문 내용입니다.",
"image": "https://doo39oi115k60.cloudfront.net/wizard-url-example/02.webp"
},
{
"label": "outro",
"text": "" // an empty string deletes that text element
}
]
}
}
Example 2 - Targeting elements by label
When a template has multiple text elements, you can use the { label, content } form to precisely specify which element receives what content. However, this approach requires that each text element in the Videostew editing screen has been pre-labeled. Elements without a label cannot be targeted. For example, if a template has the top element labeled title and the bottom AI voice element labeled voicetext, you can control each independently as shown below.
{
"data": {
"slides": [
{
"label": "main",
"text": [
{ "label": "title", "content": "이거 진짜 될까?" }, // top title element on screen
{ "label": "voicetext", "content": "이거 진짜 될까" } // subtitle element read by AI voice
]
}
]
}
}
Example 3 - Recorded narration + per-slide video
{
"data": {
"slides": [
{
"label": "intro",
"narration": "https://doo39oi115k60.cloudfront.net/wizard-url-example/narration1.mp3", // self-recorded/synthesized audio file
"video": "https://doo39oi115k60.cloudfront.net/wizard-url-example/video1.mp4"
},
{
"label": "body",
"narration": "https://doo39oi115k60.cloudfront.net/wizard-url-example/narration2.mp3",
"video": "https://doo39oi115k60.cloudfront.net/wizard-url-example/video2.mp4",
"bgm": "https://example.com/this-slide-only.mp3" // different BGM for this slide only
}
]
}
}
This is the extended mode of injector.data. Use the elements array when you need fine-grained control that cannot be expressed with the simple mode above (text/image/video strings) — such as element positioning, multiple elements of the same type, entrance animations, idle effects, and so on. The following is a complete list of available keys — include only what you need in your actual request.
baseProjectIdrequired: Injection is based on a project/template owned by the caller. If you omitbaseProjectId, elements will not be applied, so specify your own project ID. You cannot create a new slide layout that does not exist in the base usingelementsalone.
color format:
[[R, G, B, A], degree](R/G/B are 0–255, A is 0–1 opacity, degree is the gradient angle,0for solid color).
Media replacement uses a public URL in
libraryId: If you put a public URL in an image/video element'slibraryId, it is ingested and replaced. It cannot be replaced viacustom.sourceor an element'simagekey.
Element entrance animation trans (a top-level field of the element, separate from the slide transition trans. Format: group__direction):
| Group | Direction |
|---|---|
slide flip_push flip_pull cube |
left right up down |
stretch |
left right up down horizontal vertical |
flip |
horizontal_left horizontal_right vertical_up vertical_down |
reveal |
left right up down horizontal_open vertical_open |
fade |
normal left right up down |
zoom zoomspin |
in out |
circle |
open |
clock |
asc desc |
blink |
(no direction) |
Element idle effect idle: pendulum pulse twitch updown leftright shake blink clockwise counterclockwise. Intensity is set with idleConf: { "speed": number, "distance": number }.
Slide transitions and element animations use different value systems. Slide transitions (
slides[].trans.type) use hyphenated directions (fade,cube-ltr,reveal-rtl, etc.), while element entrance animations (elements[].trans.type) use the underscore directions from the table above (reveal__right, etc.).
Field tables
Slide level:
| Key | Type | Description |
|---|---|---|
label |
string | Matched by base slide label (same as simple mode) |
trans |
object | Slide transition effect { type, durationTime } |
narration |
object | Narration (below) |
glb |
object | Slide-global (BGM/sound effect/background/blur) |
elements |
array | Array of elements. Replaced on label match, created if absent, deleted when content:"" |
Element elements[] (common):
| Key | Type | Description |
|---|---|---|
type |
string | text | image | video | shape |
label |
string | Element label (selects the replacement target) |
content |
string | Text content (type:text). "" deletes the element |
libraryId |
string | Media source. A public URL is ingested and replaced |
rect |
object | Position/size (below) |
custom |
object | Style/entrance effects (below) |
trans |
object | Element entrance animation { type, txtRange } (table above) |
idle / idleConf |
string / object | Idle effect (list above) |
rect:
| Key | Description |
|---|---|
top left width height |
Position/size (px) |
opacity |
Element opacity 0–1 |
innerType |
Media display cover (fill, default) | contain (fit) | fill (original ratio) |
innerPosition |
Focus position, CSS position format (e.g., 50% 30%) |
custom (text):
| Key | Description |
|---|---|
fontSize |
Font size (px) |
color / raColor |
Text color / readability-background color (color format) |
align |
Horizontal alignment left | center | right |
valign |
Vertical alignment top | middle | bottom |
lineHeight |
Line height |
raType |
Readability background "" | block | inline (default) | outline | shadow |
enabledTts |
If true, this text is read by the AI voice |
splitLine |
Text entrance effect "" | 1l | 2l | p |
splitLineType |
replace (replace) | add (accumulate) |
custom (video):
| Key | Description |
|---|---|
trimStart trimEnd |
Trim (seconds) |
speed volume |
Speed / volume |
loop |
Loop (0 = none, 9999 = infinite) |
narration:
| Key | Description |
|---|---|
type |
voice (audio file) | tts (text synthesis) |
libraryId |
Audio URL when type:voice |
content |
Text to read when type:tts |
volume speed trimStart trimEnd |
Volume / speed / trim |
glb:
| Key | Description |
|---|---|
bgmLibraryId bgmVolume bgmSpeed |
Slide BGM URL / volume / speed |
soundLibraryId soundVolume |
Sound effect URL / volume |
soundEvery |
If true, replays across consecutive slides |
soundFit |
If true, fades to fit the slide length |
backgroundColor |
Background color (color format) |
bgBlr bgBlrB |
Background blur on/off / intensity 0–1 |
{
"data": {
"title": "프로젝트 제목",
"language": "ko",
"slides": [
{
"trans": {
"type": "fade", // auto | fade | flip-ltr | flip-ttb | cube-ltr | cube-rtl | reveal-ltr | reveal-rtl
"durationTime": 0.3
},
"narration": {
"type": "voice", // voice (audio file) | tts (text synthesis)
"libraryId": "https://doo39oi115k60.cloudfront.net/wizard-url-example/narration1.mp3", // when type is voice
"content": "", // text to read when type is tts
"volume": 1,
"speed": 1.1,
"trimStart": 0,
"trimEnd": 0
},
"glb": {
"bgmLibraryId": "https://example.com/bgm.mp3",
"bgmVolume": 0.9,
"bgmSpeed": 1,
"soundLibraryId": "https://example.com/sfx.mp3",
"soundVolume": 1,
"soundEvery": true,
"soundFit": false,
"backgroundColor": [[0, 0, 0, 1], 0],
"bgBlr": false,
"bgBlrB": 0.4
},
"elements": [
{
"type": "text",
"content": "텍스트 내용",
"rect": {
"top": 800, "left": 40, "width": 1000, "height": 200,
"opacity": 1
},
"custom": {
"fontSize": 48,
"color": [[255, 255, 255, 1], 0],
"align": "center", // left | center | right
"valign": "middle", // top | middle | bottom
"lineHeight": 1.2,
"raType": "shadow", // "" | block | inline | outline | shadow (default inline)
"raColor": [[0, 0, 0, 1], 0],
"enabledTts": true,
"splitLine": "1l", // "" all | 1l one line at a time | 2l two lines at a time | p per paragraph
"splitLineType": "replace" // replace | add (accumulate)
},
"trans": { "type": "reveal__right", "txtRange": "line" } // element entrance animation (table above)
},
{
"type": "image",
"libraryId": "https://doo39oi115k60.cloudfront.net/wizard-url-example/01.webp",
"rect": {
"top": 0, "left": 0, "width": 1080, "height": 1920,
"opacity": 1,
"innerType": "cover", // cover | contain | fill
"innerPosition": "50% 30%" // CSS position format, image focus position
},
"idle": "updown", // idle effect (list above)
"idleConf": { "speed": 1.0, "distance": 1.0 }
},
{
"type": "video",
"libraryId": "https://doo39oi115k60.cloudfront.net/wizard-url-example/video1.mp4",
"rect": {
"top": 0, "left": 0, "width": 1080, "height": 1080,
"innerType": "cover",
"innerPosition": "50% 50%"
},
"custom": {
"trimStart": 0,
"trimEnd": 10,
"speed": 1,
"volume": 1,
"loop": 9999 // 0 = no loop, 9999 = infinite loop
}
},
{
"type": "shape",
"libraryId": "shape-library-id",
"rect": {
"top": 0, "left": 0, "width": 200, "height": 200
},
"custom": {
"color": [[255, 100, 0, 1], 0]
}
}
]
}
]
}
}
Text substitution rules applied during TTS/STT. Passed at the top level of the body alongside injector. Merged with the space dictionary, with values specified here taking precedence. Maximum 100 entries per section.
| Key | Description |
|---|---|
tts |
Applied when AI voice reads text. Force-sets the correct pronunciation for words that are mispronounced |
stt |
Applied when converting speech to text (subtitle generation). Force-corrects words that STT misrecognizes |
trans |
Applied during translation post-processing |
{
"injector": {
"wizard": { "source": "url", "sourceContent": "https://doo39oi115k60.cloudfront.net/wizard-url-example/" }
},
"dictionary": {
"tts": [
{ "i": "3D", "o": "쓰리디" }, // prevents it from being read as "삼디"
{ "i": "AI", "o": "에이아이" } // prevents it from being read as "아이"
],
"stt": [
{ "i": "video studio", "o": "videostew" }, // corrects STT misrecognition
{ "i": "비디오 스튜", "o": "비디오스튜" } // corrects spacing misrecognition
]
}
}
curl -X GET "https://videostew.com/api/spaces" \
-H "Authorization: Bearer {{bearerToken}}" \
-H "x-nonce: $RANDOM"
| Parameter | Type | Description |
|---|---|---|
| spaceId | int | Target workspace ID. 0 means the default workspace |
| projectIds | string | Comma-separated list of project IDs |
| folderId | int | Folder ID. Default (root) is 1000000 |
| status | string | public | enabled | secret. trash means deleted |
| limit | int | Items per page |
| page | int | Page number |
| title | string | Search by title |
| tags | string | Search by tags |
curl -X GET "https://videostew.com/api/projects?spaceId=0&limit=20&page=1" \
-H "Authorization: Bearer {{bearerToken}}" \
-H "x-nonce: $RANDOM"
curl -X GET "https://videostew.com/api/projects/PROJECT_ID" \
-H "Authorization: Bearer {{bearerToken}}" \
-H "x-nonce: $RANDOM"
Query parameter: includeSlides=true — the response result includes a slides array (each slide: slideId, glb, duration, narration, trans, elements, note, etc., symmetric with the injection structure). Useful for verifying that injected values were actually applied. It is large, so use it only when needed.
| Parameter | Type | Description |
|---|---|---|
| spaceId | int | Target workspace ID. 0 means the default workspace |
| projectIds | string | Comma-separated list of project IDs |
| projectId | string | Retrieve exports for a specific project only |
| type | string | mp4 | jpg |
| limit | int | Items per page |
| page | int | Page number |
curl -X GET "https://videostew.com/api/exports?projectId=PROJECT_ID&limit=20" \
-H "Authorization: Bearer {{bearerToken}}" \
-H "x-nonce: $RANDOM"
curl -X GET "https://videostew.com/api/exports/PROJECT_ID/EXPORT_ID" \
-H "Authorization: Bearer {{bearerToken}}" \
-H "x-nonce: $RANDOM"
| Parameter | Type | Description |
|---|---|---|
| type | string | mp4 | jpg |
| webhookUrl | string | URL to receive the result upon completion |
| webhookType | string | form | json |
| webhookHeader | string | Custom webhook header. Format: Header-Name: value. Example: x-api-key: abc123 |
curl -X POST "https://videostew.com/api/exports/PROJECT_ID" \
-H "Authorization: Bearer {{bearerToken}}" \
-H "x-nonce: $RANDOM" \
-H "Content-Type: application/json" \
-d '{"type":"mp4","webhookUrl":"https://your-server.com/webhook"}'
Response:
{
"status": "done",
"result": {
"id": "e-19",
"projectId": "...",
"status": "generate",
"type": "mp4"
}
}
Webhook response (on export completion):
{
"status": "done",
"projectId": "...",
"size": "1280x720",
"type": "mp4",
"link": "https://cdn.videostew.com/projects/..."
}
Use this when you want to provide only the Videostew editing interface to your customers on your own website.
When using the SDK, you must issue a separate access token for each user. A token is valid for 1 month after its last use and is automatically renewed on reuse. If you receive a 401 Token is invalid response, issue a new token and handle the fallback.
!! To prevent
secretkey exposure, make this request from the backend only.
| Parameter | Required | Type | Description |
|---|---|---|---|
| apiKey | ✓ | string | The ApiKey of the issued app |
| secret | ✓ | string | The secret key of the issued app |
| uniqueId | ✓ | any | The unique identifier of the user in your platform |
| language | string | Force-set language when a new uniqueId is created. Example: ko, en |
curl -X POST "https://videostew.com/auth/access-token" \
-H "Content-Type: application/json" \
-d '{
"apiKey": "{{apiKey}}",
"secret": "{{secret}}",
"uniqueId": "user_123"
}'
Response:
{
"status": "done",
"apiKey": "abc1de2fg3...",
"token": "Qjky6mFiMkRnOmExYj..."
}
Force-expires a user's token when all activity ends (e.g., logout). Since tokens are not single-use, force-deleting one may cause issues in other open tabs. Call this when you are certain the user has stopped using the service (e.g., account deletion).
curl -X POST "https://videostew.com/auth/revoke-access-token" \
-H "Content-Type: application/json" \
-d '{
"apiKey": "{{apiKey}}",
"secret": "{{secret}}",
"uniqueId": "user_123"
}'
Opens the editor as a modal, typically from a button click or similar event.
<script src="https://cdn.videostew.com/web/sdk/sdk-latest.js"></script>
<script>
videostewSDK.open({
token: "Qjky6mFiMkRnOmEx...", // token value from the /auth/access-token response
projectId: "...", // ID of the project to edit (creates a new one if omitted)
// baseProjectId: "...", // use a specific project as a template (on new creation)
// language: "ko", // project and UI language setting
// injector: { ... }, // data injection/transformation spec
// dictionary: { ... }, // TTS/STT text substitution rules
callbackFunc: function(result) {
// called when the user clicks [Done]
console.log(result.projectId);
// result: { projectId, thumbnailUrl, spaceId, width, height, title, status, slideCount, savedAt }
},
eventFunc: function(projectId, eventName) {
// called when an event occurs in the editing screen
// eventName: openPreview, saveProject, openDownload,
// requestExportPng, requestExportMp4, exportedPng, exportedMp4,
// downloadPng, downloadMp4, openProjectInfo, updateProjectInfo
},
errorFunc: function(errorMessage) {
// called on token expiry → issue a new token and retry
// errorMessage: "Token is invalid"
}
});
</script>
| Option | Required | Description |
|---|---|---|
| token | ✓ | The token value from the /auth/access-token response. Example: Qjky6mFiMkRn... |
| apiKey | Required only when using the legacy method (raw token). Omit for the new method | |
| projectId | ID of the project to edit. Creates a new project if omitted | |
| baseProjectId | Template project ID. Must be in the same space or be public | |
| language | Project and UI language. Defaults to en if not specified |
|
| injector | Data injection/transformation spec. See Injector section | |
| dictionary | TTS/STT text substitution rules. See dictionary section | |
| callbackFunc | function(result) - called when the [Done] button is clicked. Ignored if a Done Webhook is configured in the app settings. |
|
| eventFunc | function(projectId, eventName) - called when an event occurs in the editing screen |
|
| errorFunc | function(errorMessage) - called on token expiry |
Saves the current state of the editing screen. This is equivalent to the user pressing the [Save] button, so there is generally no need to call it explicitly.
videostewSDK.save();
Saves the edited content and delivers the result to callbackFunc or the Done Webhook.
videostewSDK.done();
Closes the editor. A confirmation popup is shown if there are unsaved changes.
videostewSDK.cancel();
// videostewSDK.cancel(true); // force close