Video Merge

Combine multiple videos and images into a single video with optional audio overlay. Perfect for creating slideshows, video compilations, or adding background music.

Create a Merge Job

Create a merge job to combine multiple assets into a single video:

merge.ts
import { Stack0 } from '@stack0/sdk'
const stack0 = new Stack0({
apiKey: process.env.STACK0_API_KEY!
})
// Create a merge job
const job = await stack0.cdn.createMergeJob({
projectSlug: 'my-project',
inputs: [
{ assetId: 'intro-video-id' },
{ assetId: 'image-id', duration: 5 }, // Show image for 5 seconds
{ assetId: 'main-video-id', startTime: 10, endTime: 60 }, // Trim to 50 seconds
],
audioTrack: {
assetId: 'background-music-id',
loop: true,
fadeIn: 2,
fadeOut: 3,
},
output: {
format: 'mp4',
quality: '1080p',
filename: 'final-video.mp4',
},
webhookUrl: 'https://your-app.com/webhook',
})
console.log(`Merge job started: ${job.id}`)

Input Types

You can merge videos and images in any order. Each input can be configured with the following options:

OptionTypeDescription
assetIdstringUUID of the video or image asset (required)
durationnumberDisplay duration in seconds (required for images, max 3600)
startTimenumberTrim start point in seconds (videos only)
endTimenumberTrim end point in seconds (videos only)
textOverlayobjectText caption burned into this segment
compositeobjectComposite this input into a region of a background asset (see below)

Composite Into a Background (Screen Replacement)

Place an input clip into a defined region of a background asset — for example, corner-pin an app screen recording onto the phone in a "holding my phone" shot. Paint the screen area of the background a solid key color (e.g. #F500FA): the keyed background is layered on top of the placed overlay, so the recording shows through only on the screen and real foreground like fingers occludes it. The composited segment then joins the merge sequence like any other input.

composite.ts
const job = await stack0.cdn.createMergeJob({
projectSlug: 'my-project',
inputs: [
{ assetId: 'reaction-clip' },
{
assetId: 'app-screen-recording', // the overlay
composite: {
backgroundAssetId: 'holding-phone-shot', // image or video
// Where the overlay's corners land, in background pixels
quad: {
topLeft: { x: 320, y: 480 },
topRight: { x: 760, y: 500 },
bottomLeft: { x: 300, y: 1400 },
bottomRight: { x: 740, y: 1440 },
},
fit: 'cover', // cover | contain | fill
chromaKey: { color: '#F500FA', similarity: 0.34, blend: 0.06 },
},
},
],
output: { quality: '1080p', aspectRatio: '9:16' },
})
// Simpler axis-aligned variant (no perspective): rect picture-in-picture
const pip = await stack0.cdn.createMergeJob({
projectSlug: 'my-project',
inputs: [
{
assetId: 'app-screen-recording',
composite: {
backgroundAssetId: 'background-image',
rect: { x: 560, y: 100, width: 400, height: 300 },
fit: 'cover',
},
},
],
})

Provide exactly one of quad (perspective corner-pin — requires chromaKey or maskAssetId) or rect. A grayscale maskAssetId image can replace the chroma key: white keeps the background, black reveals the overlay. The overlay's audio is kept; output resolution matches the background.

Audio Track Overlay

Add background music or voiceover to your merged video:

audio-overlay.ts
// Add background music with fade effects
const job = await stack0.cdn.createMergeJob({
projectSlug: 'my-project',
inputs: [
{ assetId: 'video-1' },
{ assetId: 'video-2' },
{ assetId: 'video-3' },
],
audioTrack: {
assetId: 'music-track-id',
loop: true, // Loop if audio is shorter than video
fadeIn: 2, // 2 second fade in
fadeOut: 3, // 3 second fade out
},
})
OptionTypeDescription
assetIdstringUUID of the audio asset (required)
loopbooleanLoop audio if shorter than video (default: false)
fadeInnumberFade in duration in seconds (0-10)
fadeOutnumberFade out duration in seconds (0-10)

Output Configuration

Configure the output format and quality:

OptionTypeDescription
formatstringOutput format: mp4, webm (default: mp4)
qualitystringQuality preset: 360p, 480p, 720p, 1080p, 1440p, 2160p (default: 720p)
aspectRatiostringOutput canvas: auto, 16:9, 9:16, 1:1 (default: auto). With auto, the output matches the orientation of the first visual input, so portrait inputs produce portrait output (1080x1920 at 1080p) — ideal for Reels/TikTok. Explicit values force the canvas; quality sets the short edge.
filenamestringCustom filename for the output (max 255 chars)

Check Job Status

job-status.ts
// Get merge job with output asset details
const job = await stack0.cdn.getMergeJob('job-id')
console.log(`Status: ${job.status}`)
console.log(`Progress: ${job.progress}%`)
if (job.status === 'completed' && job.outputAsset) {
console.log(`Output video: ${job.outputAsset.cdnUrl}`)
console.log(`Duration: ${job.outputAsset.duration}s`)
console.log(`Size: ${job.outputAsset.size} bytes`)
}
// Status values:
// 'pending' - Job created, waiting to be processed
// 'queued' - Job queued for processing
// 'processing' - Currently merging
// 'completed' - Finished successfully
// 'failed' - Merge failed
// 'cancelled' - Job was cancelled

List Merge Jobs

list-jobs.ts
// List all merge jobs for a project
const { jobs, total, hasMore } = await stack0.cdn.listMergeJobs({
projectSlug: 'my-project',
status: 'completed', // Optional filter
limit: 20,
offset: 0,
})
for (const job of jobs) {
console.log(`${job.id}: ${job.status} (${job.progress}%)`)
}

Cancel a Merge Job

cancel-job.ts
// Cancel a pending or processing merge job
await stack0.cdn.cancelMergeJob('job-id')
console.log('Merge job cancelled')

Create a Photo Slideshow

Example: Create a slideshow from images with background music:

slideshow.ts
// Create a photo slideshow with music
const job = await stack0.cdn.createMergeJob({
projectSlug: 'my-project',
inputs: [
{ assetId: 'photo-1', duration: 3 },
{ assetId: 'photo-2', duration: 3 },
{ assetId: 'photo-3', duration: 3 },
{ assetId: 'photo-4', duration: 3 },
{ assetId: 'photo-5', duration: 3 },
],
audioTrack: {
assetId: 'background-music',
fadeIn: 1,
fadeOut: 2,
},
output: {
format: 'mp4',
quality: '1080p',
// 'auto' (default) follows the photos' orientation — portrait photos
// produce a 1080x1920 vertical video. Use '9:16' to force it.
aspectRatio: 'auto',
filename: 'vacation-slideshow.mp4',
},
})
// Poll for completion
let status = job.status
while (status === 'pending' || status === 'queued' || status === 'processing') {
await new Promise(resolve => setTimeout(resolve, 5000))
const updated = await stack0.cdn.getMergeJob(job.id)
status = updated.status
console.log(`Progress: ${updated.progress}%`)
}
const final = await stack0.cdn.getMergeJob(job.id)
if (final.status === 'completed' && final.outputAsset) {
console.log(`Slideshow ready: ${final.outputAsset.cdnUrl}`)
}

Combine Video Clips

Example: Combine multiple video clips with trimming:

combine-clips.ts
// Combine video clips with trimming
const job = await stack0.cdn.createMergeJob({
projectSlug: 'my-project',
inputs: [
// Full intro video
{ assetId: 'intro-video' },
// Trim the highlight from 30s to 90s
{ assetId: 'main-footage', startTime: 30, endTime: 90 },
// Show a title card for 5 seconds
{ assetId: 'title-card-image', duration: 5 },
// Trim outro to just the first 10 seconds
{ assetId: 'outro-video', endTime: 10 },
],
output: {
format: 'mp4',
quality: '1080p',
filename: 'highlight-reel.mp4',
},
webhookUrl: 'https://your-app.com/api/webhook/merge-complete',
})

Webhook Notifications

Receive notifications when a merge job completes:

webhook.ts
// Webhook payload example
{
"event": "merge.completed",
"jobId": "job-uuid-here",
"status": "completed",
"outputAssetId": "asset-uuid-here",
"outputUrl": "https://cdn.stack0.dev/...",
"duration": 125.5,
"createdAt": "2024-01-15T10:00:00Z",
"completedAt": "2024-01-15T10:02:30Z"
}
// Handle webhook in your app
app.post('/api/webhook/merge-complete', (req, res) => {
const { event, jobId, status, outputUrl } = req.body
if (event === 'merge.completed' && status === 'completed') {
console.log(`Merge job ${jobId} completed: ${outputUrl}`)
// Update your database, notify users, etc.
}
res.status(200).send('OK')
})

Limits

Max Inputs

100

videos/images per job

Image Duration

1 hour

max duration per image

Fade Duration

10 sec

max fade in/out