Developer API
Video Studio API
Make one MP4 from images, videos, music, text, and optional AI instructions.
For current credit costs, see API credit costs.
OpenAPI-style reference
API endpoints
Upload or reference media assets and render timeline-based videos with optional AI planning.
/api/v1 are auto-cleaned and do not count toward the user's storage quota./api/v1/media-render/uploadsCreate asset upload URLs
Creates signed upload URLs for Video Studio source assets.
Parameters
| Field | Type | Required | Details |
|---|---|---|---|
projectName | string | No | Optional project name body |
assets | array | Yes | Image, video, or audio assets to upload |
assets[].fileName | string | Yes | Asset file name body |
assets[].mediaType | image | video | audio | Yes | Asset media type |
assets[].fileSize | number | Yes | Declared size in bytes body |
Examples
Request
{
"projectName": "Launch video",
"assets": [
{
"fileName": "clip.mp4",
"mediaType": "video",
"contentType": "video/mp4",
"fileSize": 52428800
}
]
}Response
{
"projectId": "mrproj_...",
"uploads": [
{
"assetId": "asset_...",
"uploadUrl": "https://...",
"objectKey": "user_.../media-render-api/..."
}
]
}Responses
| Status | Description |
|---|---|
200 | Request succeeded |
400 | Invalid request body or unsupported parameter |
401 | Missing, invalid, or revoked API key |
429 | Rate limit exceeded |
500 | Unexpected processing error |
/api/v1/media-render/jobsStart Video Studio render
Starts a timeline render from uploaded assets, external URLs, explicit clips, text overlays, voiceover, SFX, and optional AI planning.
Parameters
| Field | Type | Required | Details |
|---|---|---|---|
projectId | string | No | Project ID from upload endpoint when using uploaded assets |
assets | array | No | Optional external URL assets bodySimple controls |
aiAnalysis | boolean | No | Let AI plan order, durations, and overlays |
instructions | string | No | AI planning instructions bodyAI planning |
targetDurationSeconds | number | No | Target render duration bodySimple controls |
aspectRatio | 9:16 | 16:9 | 1:1 | No | Output aspect ratio |
voiceover | object | No | Optional voiceover config using voiceId |
textOverlays | array | No | Optional manual text overlays and text presets |
sfxTypes | array | No | Optional automatic sound effect preset names bodySFX presets |
transitionTypes | array | No | Optional animation and transition preset names |
dynamicMotion | boolean | No | Enable dynamic motion where supported bodyDynamic motion |
bgm | boolean | No | Enable background music bodyMusic options |
Examples
Request
{
"projectId": "mrproj_...",
"aiAnalysis": true,
"instructions": "Create a short launch teaser",
"targetDurationSeconds": 30,
"aspectRatio": "9:16"
}Response
{
"projectId": "mrproj_...",
"status": "queued",
"runId": "run_...",
"estimatedCredits": 9,
"statusUrl": "/api/v1/.../jobs/mrproj_...",
"downloadUrl": "/api/v1/.../jobs/mrproj_.../download"
}Responses
| Status | Description |
|---|---|
200 | Request succeeded |
400 | Invalid request body or unsupported parameter |
401 | Missing, invalid, or revoked API key |
429 | Rate limit exceeded |
500 | Unexpected processing error |
/api/v1/media-render/jobs/{projectId}Get Video Studio render status
Returns render progress and output metadata.
Parameters
| Field | Type | Required | Details |
|---|---|---|---|
projectId | string | Yes | Video Studio project ID path |
Examples
Response
{
"projectId": "mrproj_...",
"status": "queued | processing | completed | failed",
"progress": 100,
"outputReady": true,
"creditsUsed": 9,
"errorMessage": null,
"updatedAt": "2026-06-19T14:20:00.000Z"
}Responses
| Status | Description |
|---|---|
200 | Request succeeded |
400 | Invalid request body or unsupported parameter |
401 | Missing, invalid, or revoked API key |
429 | Rate limit exceeded |
500 | Unexpected processing error |
/api/v1/media-render/jobs/{projectId}/downloadCreate Video Studio download URL
Returns a signed URL for the rendered MP4.
Parameters
| Field | Type | Required | Details |
|---|---|---|---|
projectId | string | Yes | Video Studio project ID path |
Examples
Response
{
"projectId": "mrproj_...",
"downloadUrl": "https://signed-download-url...",
"expiresAt": "2026-06-19T15:00:00.000Z",
"expiresIn": 3600,
"contentType": "video/mp4",
"fileSize": 18345678
}Responses
| Status | Description |
|---|---|
200 | Request succeeded |
400 | Invalid request body or unsupported parameter |
401 | Missing, invalid, or revoked API key |
429 | Rate limit exceeded |
500 | Unexpected processing error |
The short version
1. Send media through Subclip upload URLs, or pass public/signed media URLs directly.
2. Start the render.
3. Poll the job.
4. Download the MP4.
1. Create upload URLs
Send one files array. Required for each file: fileName, contentType, and fileSize.
curl -X POST https://www.subclip.app/api/v1/media-render/uploads \
-H "Authorization: Bearer $SUBCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"projectName": "Launch montage",
"files": [
{
"fileName": "intro.mp4",
"contentType": "video/mp4",
"fileSize": 52428800,
"durationSeconds": 12,
"width": 1920,
"height": 1080
},
{
"fileName": "product.png",
"contentType": "image/png",
"fileSize": 2048000,
"width": 1600,
"height": 1200
},
{
"fileName": "music.mp3",
"contentType": "audio/mpeg",
"fileSize": 8192000,
"durationSeconds": 90
}
]
}'{
"projectId": "mrproj_...",
"directoryPrefix": "user-id/media-render/mrproj_.../",
"uploads": [
{
"assetId": "asset_...",
"uploadUrl": "https://...",
"method": "PUT",
"objectKey": "user/media-render/...",
"mediaType": "video",
"fileName": "intro.mp4",
"expiresIn": 900
},
{
"assetId": "asset_...",
"uploadUrl": "https://...",
"method": "PUT",
"objectKey": "user/media-render/...",
"mediaType": "image",
"fileName": "product.png",
"expiresIn": 900
}
],
"mediaCounts": { "image": 1, "video": 1, "audio": 1 },
"analysisCostBasis": {
"visionAnalysis": { "images": 1, "videos": 1 },
"audioAnalysis": { "audioFiles": 1, "videos": 1 }
}
}Supported content types include common editor uploads: JPEG, PNG, GIF, WebP, SVG, HEIC, HEIF, MP4, MPEG, MOV/QuickTime, AVI, WebM, MKV, MP3, M4A, WAV, OGG, and AAC.
Images: - image/jpeg - image/png - image/gif - image/webp - image/svg+xml - image/heic - image/heif Videos: - video/mp4 - video/mpeg - video/quicktime - video/x-msvideo - video/webm - video/x-matroska Audio: - audio/mpeg - audio/mp3 - audio/mp4 - audio/m4a - audio/x-m4a - audio/wav - audio/x-wav - audio/webm - audio/ogg - audio/aac
2. Upload files
Upload each file with PUT to its matching uploadUrl. Use the same Content-Type you sent when creating the upload URL. Content-Length must match the real file size.
curl -X PUT "$UPLOAD_URL" \ -H "Content-Type: video/mp4" \ -H "Content-Length: 52428800" \ --data-binary "@intro.mp4"
3. Start render
Smallest request: pass the projectId. Subclip sorts files by filename, shows each visual for 3 seconds, adds transitions, and ends on the last visual.
curl -X POST https://www.subclip.app/api/v1/media-render/jobs \
-H "Authorization: Bearer $SUBCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"projectId": "mrproj_...",
"aspectRatio": "9:16",
"outputFileName": "rendered.mp4"
}'You can also start from a Subclip media-render directory prefix instead of an upload project.
curl -X POST https://www.subclip.app/api/v1/media-render/jobs \
-H "Authorization: Bearer $SUBCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"projectName": "Launch montage",
"sourceDirectoryPrefix": "user-id/media-render/mrproj_.../",
"instructions": "Make a fast product launch montage.",
"aspectRatio": "9:16",
"targetDurationSeconds": 30,
"aiAnalysis": false,
"visionAnalysis": false,
"audioAnalysis": false,
"textOnScreen": false,
"bgm": true,
"bgmQuery": "upbeat product launch background music",
"bgmVolume": 0.18,
"sfx": true,
"sfxTypes": ["click", "camera-shutter"],
"sfxVolume": 0.55,
"transitionTypes": ["crossfade", "slide", "pan-left"],
"outputFileName": "launch-montage.mp4"
}'Or pass direct public/downloadable signed URLs with mediaAssets. This works with S3 presigned GET URLs when they remain downloadable through the render.
curl -X POST https://www.subclip.app/api/v1/media-render/jobs \
-H "Authorization: Bearer $SUBCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"projectName": "Signed URL montage",
"aspectRatio": "9:16",
"dynamicMotion": true,
"mediaAssets": [
{
"assetId": "hero_image",
"url": "https://your-bucket.s3.amazonaws.com/hero.png?X-Amz-Signature=...",
"fileName": "hero.png",
"mediaType": "image",
"contentType": "image/png",
"durationSeconds": 4,
"width": 1600,
"height": 1200,
"dynamicMotion": true
},
{
"assetId": "demo_video",
"url": "https://your-bucket.s3.amazonaws.com/demo.mp4?X-Amz-Signature=...",
"fileName": "demo.mp4",
"mediaType": "video",
"contentType": "video/mp4",
"durationSeconds": 8
}
],
"transitionTypes": ["crossfade", "pan-left", "bounce-flash"],
"outputFileName": "signed-url-montage.mp4"
}'Prefer Node.js for uploads and request setup:
import { createReadStream, statSync } from "node:fs";
const stats = statSync("./intro.mp4");
await fetch(uploadUrl, {
method: "PUT",
headers: {
"Content-Type": "video/mp4",
"Content-Length": String(stats.size),
},
body: createReadStream("./intro.mp4"),
duplex: "half",
});curl -X POST https://www.subclip.app/api/v1/media-render/jobs \
-H "Authorization: Bearer $SUBCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"projectId": "mrproj_...",
"instructions": "Open with the product shot, then cut into the demo clips. Keep it energetic.",
"aspectRatio": "9:16",
"aiAnalysis": true,
"visionAnalysis": true,
"audioAnalysis": false,
"textOnScreen": true,
"assetDurations": [
{ "assetId": "asset_intro", "durationSeconds": 4 },
{ "fileName": "product.png", "durationSeconds": 3.5 }
],
"bgm": true,
"bgmFileName": "music.mp3",
"bgmVolume": 0.18,
"sfx": true,
"sfxTypes": ["click", "camera-shutter"],
"sfxVolume": 0.55,
"transitionTypes": ["crossfade", "pan-left", "sway-in", "bounce-flash"],
"outputFileName": "launch-montage.mp4"
}'Simple controls
{
"projectId": "mrproj_...",
"aspectRatio": "9:16",
"aiAnalysis": false,
"visionAnalysis": false,
"audioAnalysis": false,
"textOnScreen": false,
"hookText": "The product finally makes sense",
"bgm": true,
"bgmFileName": "music.mp3",
"bgmVolume": 0.18,
"sfx": true,
"sfxTypes": ["click", "camera-shutter"],
"sfxVolume": 0.55,
"transitionTypes": ["crossfade", "pan-left"],
"outputFileName": "launch-montage.mp4"
}Hook-only request example:
{
"projectId": "mrproj_...",
"aspectRatio": "9:16",
"aiAnalysis": false,
"hookText": "The product finally makes sense",
"bgm": true,
"bgmFileName": "music.mp3",
"sfx": true,
"sfxTypes": ["click", "camera-shutter"],
"sfxVolume": 0.55,
"transitionTypes": ["crossfade", "sway-in"]
}Let AI cook
{
"projectId": "mrproj_...",
"instructions": "Create an energetic product launch montage. Build a story across all assets. Add Snapchat-style text on each clip transition so the message continues until the end. Use the UI screenshots first, then the lifestyle shots.",
"aspectRatio": "9:16",
"aiAnalysis": true,
"visionAnalysis": true,
"audioAnalysis": true,
"textOnScreen": true,
"bgm": true,
"bgmQuery": "upbeat product launch background music",
"bgmVolume": 0.18,
"sfx": true,
"sfxTypes": ["click", "camera-shutter", "whoosh"],
"sfxVolume": 0.55,
"transitionTypes": ["light-leak", "pan-left", "spin-boomerang", "bounce-flash"]
}Advanced Controls for Pro users
Exact clip timing
Use clipPlacements when you want to choose where each clip starts and ends. Match by assetId or fileName. These timings win over AI planning and can set dynamicMotion per image.
{
"projectId": "mrproj_...",
"aspectRatio": "9:16",
"aiAnalysis": false,
"clipPlacements": [
{
"fileName": "01-hook.png",
"startTime": 0,
"durationSeconds": 3,
"transitionType": "none"
},
{
"fileName": "02-proof.mp4",
"startTime": 3,
"durationSeconds": 4,
"sourceStartTime": 1,
"sourceEndTime": 5,
"transitionType": "crossfade"
},
{
"fileName": "03-result.png",
"startTime": 7,
"durationSeconds": 3,
"transitionType": "light-leak"
}
],
"sfx": true,
"sfxTypes": ["click", "camera-shutter"]
}Exact text overlays
Use textOverlays when you want to place text yourself. Pass text, timing, preset name, and top/middle/bottom position.
{
"projectId": "mrproj_...",
"aspectRatio": "9:16",
"textOverlays": [
{
"text": "This is the hook",
"startTime": 0,
"endTime": 2.8,
"templateName": "Snapchat Hook",
"position": "top"
},
{
"text": "Now the proof is clear",
"startTime": 3,
"durationSeconds": 3,
"templateName": "Headline Bar",
"position": "middle"
},
{
"text": "The result speaks for itself",
"startTime": 7,
"durationSeconds": 3,
"templateName": "Simple",
"position": "bottom"
}
]
}Exact durations only
Use assetDurations when you only need each visual to stay longer or shorter.
{
"projectId": "mrproj_...",
"instructions": "Use this exact pacing.",
"aspectRatio": "9:16",
"aiAnalysis": true,
"visionAnalysis": true,
"textOnScreen": true,
"assetDurations": [
{ "fileName": "01-hero.png", "durationSeconds": 4 },
{ "fileName": "02-dashboard.png", "durationSeconds": 3 },
{ "fileName": "03-proof.png", "durationSeconds": 5 }
],
"bgm": true,
"bgmFileName": "music.mp3",
"sfx": true,
"sfxTypes": ["camera-shutter"],
"transitionTypes": ["fade", "rise-up"]
}Voiceover
First list available voices, then pass a returned voiceoverVoiceId. You can filter by language and optional gender. Script can be plain text, SRT, or JSON text segments.
curl "https://www.subclip.app/api/v1/media-render/voices?language=en&gender=female" \ -H "Authorization: Bearer $SUBCLIP_API_KEY"
{
"voices": [
{
"id": "dvoice_...",
"name": "My launch narrator",
"voiceoverVoiceId": "dvoice_...",
"language": "en",
"source": "saved",
"gender": "female",
"createdAt": "2026-06-10T10:00:00.000Z",
"updatedAt": "2026-06-10T10:00:00.000Z"
},
{
"id": "svoice_...",
"name": "Warm Presenter",
"voiceoverVoiceId": "svoice_...",
"language": "en",
"source": "system",
"gender": "female",
"accent": "American",
"tags": ["narration", "warm"]
}
]
}{
"projectId": "mrproj_...",
"instructions": "Make a product launch story from these visuals. Keep it clear and confident.",
"aspectRatio": "9:16",
"aiAnalysis": true,
"visionAnalysis": true,
"audioAnalysis": false,
"textOnScreen": true,
"voiceover": true,
"voiceoverVoiceId": "dvoice_...",
"voiceoverLanguage": "en-US",
"voiceoverScript": {
"format": "text",
"content": "This is where the launch starts. Then the product proves why it matters. By the end, the workflow feels simple."
},
"bgm": true,
"bgmQuery": "confident product launch background music",
"sfx": true,
"sfxTypes": ["click", "camera-shutter"]
}{
"projectId": "mrproj_...",
"voiceover": true,
"voiceoverVoiceId": "dvoice_...",
"voiceoverScript": {
"format": "json",
"segments": [
{ "text": "This is where the launch starts." },
{ "text": "Then the product proves why it matters." },
{ "text": "By the end, the workflow feels simple." }
]
},
"aiAnalysis": true,
"visionAnalysis": true,
"textOnScreen": false
}Option reference
These are the exact values you can pass for text, SFX, and transitions.
Text overlays
Pick the style first, then pass its name in templateName.
Use textOverlays when you want exact text at exact times. Each overlay supports text, startTime, endTime or durationSeconds, templateName or templateId, and position.
{
"textOverlays": [
{
"text": "Your line here",
"startTime": 0,
"durationSeconds": 3,
"templateName": "Snapchat Hook",
"position": "top"
}
]
}Common templateName values
| # | Value | What it does | Preview |
|---|---|---|---|
| 1 | Snapchat Hook | Bold white hook near the top. | This changes everything |
| 2 | Headline Bar | Compact white headline bar. | Headline Bar |
| 3 | Cloud Tag | Small rounded label. | Cloud Tag |
| 4 | Midnight Strip | Dark strip with white text. | Midnight Strip |
| 5 | Title | Large title text. | Title |
| 6 | Simple | Readable boxed text. | Simple |
| 7 | Bold | Heavy outlined text. | Bold |
position can be top, middle, or bottom. If you pass an unsupported templateName or templateId, the API returns 400 invalid_request.
SFX
Listen first, then pass the values you want in sfxTypes.
Auto mode: set sfx to true, then choose sfxTypes. If you omit sfxTypes, Subclip uses click and camera-shutter.
{
"sfx": true,
"sfxTypes": ["click", "camera-shutter"],
"sfxVolume": 0.55
}Manual mode: use sfxPlacements when you want a specific sound at a specific clip start or exact timeline time. When sfxPlacements is passed, Subclip uses those manual SFX events instead of automatic transition SFX.
{
"sfxPlacements": [
{
"sfxType": "camera-shutter",
"clipFileName": "02-proof.mp4",
"offsetSeconds": 0,
"volume": 0.6
},
{
"sfxType": "whoosh",
"startTime": 7.2,
"durationSeconds": 0.8,
"volume": 0.45
}
]
}sfxType: required SFX value.
startTime: exact timeline time in seconds.
clipAssetId or clipFileName: use this when the sound should start at a clip start.
offsetSeconds: optional offset from the clip start.
durationSeconds: optional SFX trim length.
volume: optional per-placement number from 0 to 1.
sfxVolume: optional default SFX volume from 0 to 1.
Allowed sfxTypes
| # | Value | What it does | Preview |
|---|---|---|---|
| 1 | click | Small UI click. Good default. | |
| 2 | camera-shutter | Camera/photo cut sound. Good for image changes. | |
| 3 | whoosh | Bigger motion sound. Use sparingly. | |
| 4 | pop | Light pop. | |
| 5 | hit | Stronger impact sound. | |
| 6 | riser | Build-up sound. | |
| 7 | glitch | Digital glitch. | |
| 8 | notification | Phone notification. | |
| 9 | typing | Keyboard typing. | |
| 10 | processing | Loading or processing sound. |
Transitions
Preview the motion first, then pass the values you want in transitionTypes.
Set transitionTypes to the list you want. Subclip rotates through only those values. Use ["none"] for hard cuts. For manual timelines, pass clipPlacements[].transitionType to override one clip's incoming transition.
{
"transitionTypes": ["crossfade", "pan-left", "sway-in", "bounce-flash"]
}Allowed transitionTypes
| # | Value | What it does | Preview |
|---|---|---|---|
| 1 | none | No transition. Hard cut. | |
| 2 | crossfade | Soft fade between clips. | |
| 3 | fade | Simple fade. | |
| 4 | light-leak | Bright flash-style transition. | |
| 5 | dip-to-black | Fades through black. | |
| 6 | slide | Slide movement. | |
| 7 | wipe | Wipe movement. | |
| 8 | flip | Flip movement. | |
| 9 | iris | Iris-style reveal. | |
| 10 | zoom-in | Pushes into the next clip. | |
| 11 | zoom-out | Pulls out from the clip. | |
| 12 | pan-left | Pans the next clip in from the right. | |
| 13 | pan-up | Pans the next clip up from below. | |
| 14 | sway-in | Slides in with a small rotation. | |
| 15 | spin-clockwise | Rotates clockwise into place. | |
| 16 | spin-counter-clockwise | Rotates counter-clockwise into place. | |
| 17 | spin-boomerang | Quick springy spin accent. | |
| 18 | fall-left | Drops in with leftward motion. | |
| 19 | fall-right | Drops in with rightward motion. | |
| 20 | fall-down | Drops down into place. | |
| 21 | rise-up | Rises up into place. | |
| 22 | rise-left | Rises with leftward motion. | |
| 23 | rise-right | Rises with rightward motion. | |
| 24 | rise-down | Rises down from above. | |
| 25 | bounce-flash | Bounce accent with flash. |
Dynamic motion for images
Set top-level dynamicMotion to true to apply full-clip pan/zoom motion to every image clip. Override per image with dynamicMotion in mediaAssets, assetDurations, or clipPlacements. Video and audio assets do not render dynamic motion.
{
"dynamicMotion": true,
"mediaAssets": [
{
"assetId": "hero_image",
"url": "https://your-bucket.s3.amazonaws.com/hero.png",
"fileName": "hero.png",
"mediaType": "image",
"contentType": "image/png",
"durationSeconds": 4,
"dynamicMotion": true
}
],
"assetDurations": [
{ "fileName": "01-hero.png", "durationSeconds": 4, "dynamicMotion": true },
{ "fileName": "02-static.png", "durationSeconds": 3, "dynamicMotion": false }
],
"clipPlacements": [
{ "fileName": "03-detail.png", "startTime": 7, "durationSeconds": 4, "transitionType": "rise-up", "dynamicMotion": true }
]
}Music
bgmFileName or bgmAssetId: use an uploaded audio file as BGM.
bgmQuery: let Subclip pick curated BGM.
bgmVolume: optional music volume from 0 to 1.
When bgm is enabled and every visual does not have a manual duration, Subclip can sync clip timing to stronger music beats.
Voiceover
GET /api/v1/media-render/voices: lists saved voices plus Subclip catalog voices. Optional filters: language and gender.
voiceoverVoiceId: required when voiceover is true. Use a value returned by the voices endpoint.
voiceoverLanguage: optional language hint, for example en-US.
voiceoverScript: required when voiceover is true. Use plain text, SRT, or JSON text segments. Do not send segment start/end times; Subclip derives them after speech generation.
If voiceoverVoiceId is not returned by the voices endpoint, the API returns 400 invalid_voiceover_voice_id before the render starts.
Rules and limits
You must provide projectId, sourceDirectoryPrefix, or mediaAssets.
mediaAssets URLs must be http/https public or signed URLs, not localhost or private network URLs.
If a mediaAssets URL has no recognized extension, pass contentType or mediaType.
sourceDirectoryPrefix must be a Subclip media-render directory created for the API key user.
You must provide at least one image or video.
Rate limits are per API key: uploads 20/min, job starts 10/min, voice listing 60/min, status polling 120/min, download URL requests 60/min.
If bgm is true, pass bgmAssetId, bgmFileName, or bgmQuery.
If voiceover is true, pass a voiceoverVoiceId returned by GET /api/v1/media-render/voices.
clipPlacements must include assetId or fileName.
textOverlays must include text and startTime.
templateName must be one of the allowed text templates.
sfxPlacements must include startTime, clipAssetId, or clipFileName.
Volume fields must be numbers from 0 to 1.
Unknown top-level request fields return 400 invalid_request.
Manual clipPlacements win over AI timing.
Manual textOverlays render even when AI text generation is off.
Manual sfxPlacements replace automatic transition SFX.
Outputs and uploaded inputs are deleted after the scheduled cleanup window.
4. Poll status
Poll every 5 seconds for normal renders. For longer videos or heavier renders, poll every 15 seconds. Do not poll in a tight loop; if you receive 429 rate_limited, wait until X-RateLimit-Reset before trying again.
curl https://www.subclip.app/api/v1/media-render/jobs/mrproj_... \ -H "Authorization: Bearer $SUBCLIP_API_KEY"
{
"projectId": "mrproj_...",
"status": "completed",
"progress": 100,
"outputReady": true,
"creditsUsed": 4.5,
"errorMessage": null
}5. Download result
The API returns a short-lived signed URL. Download that URL to get the rendered MP4. If the render is still queued or processing, this endpoint returns 409 output_not_ready; poll status until outputReady is true.
DOWNLOAD_JSON=$(curl -s https://www.subclip.app/api/v1/media-render/jobs/mrproj_.../download \ -H "Authorization: Bearer $SUBCLIP_API_KEY") DOWNLOAD_URL=$(echo "$DOWNLOAD_JSON" | jq -r '.downloadUrl') curl -L "$DOWNLOAD_URL" -o rendered.mp4
Reference snippet
Use this full Node.js flow when you prefer one script.
const apiKey = process.env.SUBCLIP_API_KEY;
const uploads = await fetch("https://www.subclip.app/api/v1/media-render/uploads", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
projectName: "Launch montage",
files: [
{ fileName: "intro.mp4", contentType: "video/mp4", fileSize: 52428800, durationSeconds: 12 },
{ fileName: "product.png", contentType: "image/png", fileSize: 2048000 },
],
}),
}).then((r) => r.json());
// Upload every file to its matching uploads.uploads[i].uploadUrl.
// The API creates assetId, objectKey, directoryPrefix, and uploadUrl for every file in bulk.
// For Node streams, set Content-Length and duplex: "half".
await fetch("https://www.subclip.app/api/v1/media-render/jobs", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
projectId: uploads.projectId,
instructions: "Make a fast launch montage.",
aspectRatio: "9:16",
aiAnalysis: true,
visionAnalysis: true,
textOnScreen: true,
assetDurations: [
{ fileName: "intro.mp4", durationSeconds: 4 },
{ fileName: "product.png", durationSeconds: 3 },
],
bgm: true,
bgmQuery: "upbeat product launch background music",
sfx: true,
sfxTypes: ["click", "camera-shutter"],
transitionTypes: ["crossfade", "pan-left", "sway-in", "bounce-flash"],
}),
});
let job;
do {
await new Promise((resolve) => setTimeout(resolve, 5000));
job = await fetch(`https://www.subclip.app/api/v1/media-render/jobs/${uploads.projectId}`, {
headers: { Authorization: `Bearer ${apiKey}` },
}).then((r) => r.json());
} while (job.status !== "completed" && job.status !== "failed");
if (job.status === "failed") throw new Error(job.errorMessage || "Render failed");
const download = await fetch(`https://www.subclip.app/api/v1/media-render/jobs/${uploads.projectId}/download`, {
headers: { Authorization: `Bearer ${apiKey}` },
}).then((r) => r.json());
console.log(download.downloadUrl);