Subclip Logo

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 storage handling: files uploaded or generated through /api/v1 are auto-cleaned and do not count toward the user's storage quota.
POST/api/v1/media-render/uploads

Create asset upload URLs

Creates signed upload URLs for Video Studio source assets.

Bearer auth

Parameters

FieldTypeRequiredDetails
projectNamestringNo
Optional project name
body
assetsarrayYes
Image, video, or audio assets to upload
assets[].fileNamestringYes
Asset file name
body
assets[].mediaTypeimage | video | audioYes
Asset media type
assets[].fileSizenumberYes
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

StatusDescription
200Request succeeded
400Invalid request body or unsupported parameter
401Missing, invalid, or revoked API key
429Rate limit exceeded
500Unexpected processing error
POST/api/v1/media-render/jobs

Start Video Studio render

Starts a timeline render from uploaded assets, external URLs, explicit clips, text overlays, voiceover, SFX, and optional AI planning.

Bearer auth

Parameters

FieldTypeRequiredDetails
projectIdstringNo
Project ID from upload endpoint when using uploaded assets
assetsarrayNo
Optional external URL assets
aiAnalysisbooleanNo
Let AI plan order, durations, and overlays
bodydefault: falseAI planning
instructionsstringNo
AI planning instructions
targetDurationSecondsnumberNo
Target render duration
aspectRatio9:16 | 16:9 | 1:1No
Output aspect ratio
bodydefault: 9:16Simple controls
voiceoverobjectNo
Optional voiceover config using voiceId
textOverlaysarrayNo
Optional manual text overlays and text presets
sfxTypesarrayNo
Optional automatic sound effect preset names
transitionTypesarrayNo
Optional animation and transition preset names
dynamicMotionbooleanNo
Enable dynamic motion where supported
bgmbooleanNo
Enable background music

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

StatusDescription
200Request succeeded
400Invalid request body or unsupported parameter
401Missing, invalid, or revoked API key
429Rate limit exceeded
500Unexpected processing error
GET/api/v1/media-render/jobs/{projectId}

Get Video Studio render status

Returns render progress and output metadata.

Bearer auth

Parameters

FieldTypeRequiredDetails
projectIdstringYes
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

StatusDescription
200Request succeeded
400Invalid request body or unsupported parameter
401Missing, invalid, or revoked API key
429Rate limit exceeded
500Unexpected processing error
GET/api/v1/media-render/jobs/{projectId}/download

Create Video Studio download URL

Returns a signed URL for the rendered MP4.

Bearer auth

Parameters

FieldTypeRequiredDetails
projectIdstringYes
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

StatusDescription
200Request succeeded
400Invalid request body or unsupported parameter
401Missing, invalid, or revoked API key
429Rate limit exceeded
500Unexpected 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

instructions
Tell Subclip what kind of video to make.
aspectRatio
Choose 9:16, 16:9, or 1:1.
hookText
Add one hook line on the first clip.
outputFileName
Choose the final MP4 filename.
bgm
Add music. Use bgmFileName, bgmAssetId, or bgmQuery.
bgmVolume
Optional music volume from 0 to 1. Default is 0.16.
sfx
Add transition sounds.
sfxVolume
Optional default SFX volume from 0 to 1. Default is 0.58.
transitionTypes
Choose transition styles to rotate through.
dynamicMotion
Enable full-clip pan/zoom motion on image clips.
mediaAssets
Pass direct public or signed image/video/audio URLs instead of Subclip uploads.
{
  "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

aiAnalysis
Let Subclip plan order, duration, BGM vibe, and text ideas.
visionAnalysis
Let AI inspect images and videos.
audioAnalysis
Let AI transcribe audio or video speech.
textOnScreen
Let AI create text overlays.
{
  "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

#ValueWhat it doesPreview
1Snapchat HookBold white hook near the top.
This changes everything
2Headline BarCompact white headline bar.
Headline Bar
3Cloud TagSmall rounded label.
Cloud Tag
4Midnight StripDark strip with white text.
Midnight Strip
5TitleLarge title text.
Title
6SimpleReadable boxed text.
Simple
7BoldHeavy 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

#ValueWhat it doesPreview
1clickSmall UI click. Good default.
2camera-shutterCamera/photo cut sound. Good for image changes.
3whooshBigger motion sound. Use sparingly.
4popLight pop.
5hitStronger impact sound.
6riserBuild-up sound.
7glitchDigital glitch.
8notificationPhone notification.
9typingKeyboard typing.
10processingLoading 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

#ValueWhat it doesPreview
1noneNo transition. Hard cut.
2crossfadeSoft fade between clips.
3fadeSimple fade.
4light-leakBright flash-style transition.
5dip-to-blackFades through black.
6slideSlide movement.
7wipeWipe movement.
8flipFlip movement.
9irisIris-style reveal.
10zoom-inPushes into the next clip.
11zoom-outPulls out from the clip.
12pan-leftPans the next clip in from the right.
13pan-upPans the next clip up from below.
14sway-inSlides in with a small rotation.
15spin-clockwiseRotates clockwise into place.
16spin-counter-clockwiseRotates counter-clockwise into place.
17spin-boomerangQuick springy spin accent.
18fall-leftDrops in with leftward motion.
19fall-rightDrops in with rightward motion.
20fall-downDrops down into place.
21rise-upRises up into place.
22rise-leftRises with leftward motion.
23rise-rightRises with rightward motion.
24rise-downRises down from above.
25bounce-flashBounce 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

1

You must provide projectId, sourceDirectoryPrefix, or mediaAssets.

2

mediaAssets URLs must be http/https public or signed URLs, not localhost or private network URLs.

3

If a mediaAssets URL has no recognized extension, pass contentType or mediaType.

4

sourceDirectoryPrefix must be a Subclip media-render directory created for the API key user.

5

You must provide at least one image or video.

6

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.

7

If bgm is true, pass bgmAssetId, bgmFileName, or bgmQuery.

8

If voiceover is true, pass a voiceoverVoiceId returned by GET /api/v1/media-render/voices.

9

clipPlacements must include assetId or fileName.

10

textOverlays must include text and startTime.

11

templateName must be one of the allowed text templates.

12

sfxPlacements must include startTime, clipAssetId, or clipFileName.

13

Volume fields must be numbers from 0 to 1.

14

Unknown top-level request fields return 400 invalid_request.

15

Manual clipPlacements win over AI timing.

16

Manual textOverlays render even when AI text generation is off.

17

Manual sfxPlacements replace automatic transition SFX.

18

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);