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:
import { Stack0 } from '@stack0/sdk'const stack0 = new Stack0({apiKey: process.env.STACK0_API_KEY!})// Create a merge jobconst 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:
| Option | Type | Description |
|---|---|---|
| assetId | string | UUID of the video or image asset (required) |
| duration | number | Display duration in seconds (required for images, max 3600) |
| startTime | number | Trim start point in seconds (videos only) |
| endTime | number | Trim end point in seconds (videos only) |
| textOverlay | object | Text caption burned into this segment |
| composite | object | Composite 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.
const job = await stack0.cdn.createMergeJob({projectSlug: 'my-project',inputs: [{ assetId: 'reaction-clip' },{assetId: 'app-screen-recording', // the overlaycomposite: {backgroundAssetId: 'holding-phone-shot', // image or video// Where the overlay's corners land, in background pixelsquad: {topLeft: { x: 320, y: 480 },topRight: { x: 760, y: 500 },bottomLeft: { x: 300, y: 1400 },bottomRight: { x: 740, y: 1440 },},fit: 'cover', // cover | contain | fillchromaKey: { color: '#F500FA', similarity: 0.34, blend: 0.06 },},},],output: { quality: '1080p', aspectRatio: '9:16' },})// Simpler axis-aligned variant (no perspective): rect picture-in-pictureconst 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:
// Add background music with fade effectsconst 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 videofadeIn: 2, // 2 second fade infadeOut: 3, // 3 second fade out},})
| Option | Type | Description |
|---|---|---|
| assetId | string | UUID of the audio asset (required) |
| loop | boolean | Loop audio if shorter than video (default: false) |
| fadeIn | number | Fade in duration in seconds (0-10) |
| fadeOut | number | Fade out duration in seconds (0-10) |
Output Configuration
Configure the output format and quality:
| Option | Type | Description |
|---|---|---|
| format | string | Output format: mp4, webm (default: mp4) |
| quality | string | Quality preset: 360p, 480p, 720p, 1080p, 1440p, 2160p (default: 720p) |
| aspectRatio | string | Output 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. |
| filename | string | Custom filename for the output (max 255 chars) |
Check Job Status
// Get merge job with output asset detailsconst 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 all merge jobs for a projectconst { jobs, total, hasMore } = await stack0.cdn.listMergeJobs({projectSlug: 'my-project',status: 'completed', // Optional filterlimit: 20,offset: 0,})for (const job of jobs) {console.log(`${job.id}: ${job.status} (${job.progress}%)`)}
Cancel a Merge Job
// Cancel a pending or processing merge jobawait stack0.cdn.cancelMergeJob('job-id')console.log('Merge job cancelled')
Create a Photo Slideshow
Example: Create a slideshow from images with background music:
// Create a photo slideshow with musicconst 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 completionlet status = job.statuswhile (status === 'pending' || status === 'queued' || status === 'processing') {await new Promise(resolve => setTimeout(resolve, 5000))const updated = await stack0.cdn.getMergeJob(job.id)status = updated.statusconsole.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 video clips with trimmingconst 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 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 appapp.post('/api/webhook/merge-complete', (req, res) => {const { event, jobId, status, outputUrl } = req.bodyif (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