Skip to main content

Quickstart: Node.js

Render a video from Node.js using the native fetch API (Node 18+) or any HTTP client.


Prerequisites

  • Node.js 18 or later (node --version)
  • A ViralSync API key (Pro plan or higher)
  • Your project ID from Dashboard → Projects

Basic example: submit and poll

// render.js
const API_KEY = process.env.VIRALSYNC_API_KEY;
const PROJECT_ID = process.env.VIRALSYNC_PROJECT_ID;
const BASE_URL = 'https://api.viralsync.io';

const headers = {
'x-api-key': API_KEY,
'Content-Type': 'application/json',
};

async function submitRender() {
const payload = {
kind: 'movie',
projectId: PROJECT_ID,
movie: {
width: 1920,
height: 1080,
fps: 30,
scenes: [
{
id: 'scene_1',
duration: 5,
bgColor: '#1a1a2e',
layers: [
{
id: 'title',
type: 'text',
text: 'Hello from Node.js',
startTime: 0,
duration: 5,
zIndex: 1,
position: { x: 360, y: 460 },
size: { width: 1200, height: 160 },
fontSize: 80,
fontWeight: 'bold',
color: '#00d4ff',
textAlign: 'center',
},
],
},
],
},
};

const res = await fetch(`${BASE_URL}/v1/render/jobs`, {
method: 'POST',
headers,
body: JSON.stringify(payload),
});

if (!res.ok) {
const err = await res.json();
throw new Error(`Submit failed: [${err.code}] ${err.error}`);
}

const { jobId } = await res.json();
return jobId;
}

async function pollUntilDone(jobId) {
while (true) {
const res = await fetch(`${BASE_URL}/v1/jobs/${jobId}`, { headers });
const job = await res.json();

const pct = Math.round((job.progress ?? 0) * 100);
console.log(`[${job.status}] ${pct}%${job.phase ? `${job.phase}` : ''}`);

if (job.status === 'done') return job.outputUrl;
if (job.status === 'failed') throw new Error(`Render failed: ${job.error}`);
if (job.status === 'cancelled') throw new Error('Job was cancelled');

await new Promise((r) => setTimeout(r, 3000));
}
}

async function main() {
console.log('Submitting render job...');
const jobId = await submitRender();
console.log(`Job queued: ${jobId}`);

console.log('Waiting for completion...');
const outputUrl = await pollUntilDone(jobId);
console.log(`\nVideo ready: ${outputUrl}`);
}

main().catch(console.error);

Run it:

VIRALSYNC_API_KEY=vs_prod_YOUR_KEY \
VIRALSYNC_PROJECT_ID=proj_abc123 \
node render.js

Real-time progress with Server-Sent Events

For a better user experience, stream progress instead of polling:

import EventSource from 'eventsource'; // npm install eventsource

async function streamProgress(jobId) {
return new Promise((resolve, reject) => {
const url = `https://api.viralsync.io/v1/jobs/${jobId}/progress`;
const source = new EventSource(url, {
headers: { 'x-api-key': process.env.VIRALSYNC_API_KEY },
});

source.onmessage = (event) => {
const update = JSON.parse(event.data);
const pct = Math.round((update.progress ?? 0) * 100);
process.stdout.write(`\r[${update.status}] ${pct}% `);

if (update.status === 'done') {
console.log(`\nDone: ${update.outputUrl}`);
source.close();
resolve(update.outputUrl);
} else if (update.status === 'failed') {
source.close();
reject(new Error('Render failed'));
} else if (update.status === 'cancelled') {
source.close();
reject(new Error('Job cancelled'));
}
};

source.onerror = () => {
// SSE connection dropped — reconnect after brief delay
source.close();
setTimeout(() => streamProgress(jobId).then(resolve).catch(reject), 2000);
};
});
}

// Use in place of pollUntilDone:
const outputUrl = await streamProgress(jobId);

TypeScript example

// render.ts
interface Layer {
id: string;
type: 'video' | 'image' | 'audio' | 'text';
startTime: number;
duration: number;
zIndex: number;
position: { x: number; y: number };
size: { width: number; height: number };
source?: string;
text?: string;
fontSize?: number;
fontWeight?: string | number;
color?: string;
textAlign?: 'left' | 'center' | 'right';
transform?: { opacity?: number; rotation?: number; scale?: number };
mute?: boolean;
volume?: number;
}

interface Scene {
id: string;
duration: number;
bgColor?: string;
layers: Layer[];
}

interface Movie {
width: number;
height: number;
fps: number;
scenes: Scene[];
}

interface RenderJobRequest {
kind: 'movie';
projectId: string;
movie: Movie;
renderOptions?: {
qualityPreset?: 'stable' | 'balanced' | 'fast' | 'turbo';
};
callbackUrl?: string;
}

interface JobResponse {
jobId: string;
status: 'queued' | 'running' | 'done' | 'failed' | 'cancelled';
progress?: number;
phase?: string;
outputUrl?: string;
error?: string;
}

async function submitRender(payload: RenderJobRequest): Promise<string> {
const res = await fetch('https://api.viralsync.io/v1/render/jobs', {
method: 'POST',
headers: {
'x-api-key': process.env.VIRALSYNC_API_KEY!,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});

if (!res.ok) {
const err = await res.json() as { error: string; code: string };
throw new Error(`[${err.code}] ${err.error}`);
}

const { jobId } = await res.json() as { jobId: string };
return jobId;
}

Error handling

async function submitWithRetry(payload, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const res = await fetch('https://api.viralsync.io/v1/render/jobs', {
method: 'POST',
headers,
body: JSON.stringify(payload),
});

if (res.status === 429) {
const retryAfter = parseInt(res.headers.get('Retry-After') ?? '5', 10);
if (attempt < maxAttempts) {
console.log(`Rate limited. Waiting ${retryAfter}s...`);
await new Promise((r) => setTimeout(r, retryAfter * 1000));
continue;
}
}

if (res.status === 409) {
const { code } = await res.json();
if (code === 'CONCURRENCY_LIMIT_EXCEEDED') {
console.log('Concurrency limit reached. Waiting 10s...');
await new Promise((r) => setTimeout(r, 10000));
continue;
}
}

if (!res.ok) {
const err = await res.json();
throw new Error(`[${err.code}] ${err.error}`);
}

return (await res.json()).jobId;
}
}

Using with Express webhook handler

import express from 'express';

const app = express();
app.use(express.json());

app.post('/webhooks/viralsync', (req, res) => {
// Respond 200 immediately — process asynchronously
res.sendStatus(200);

const { jobId, status, outputUrl, error } = req.body;

if (status === 'done') {
console.log(`Job ${jobId} complete. Video: ${outputUrl}`);
// Download, store, notify user, etc.
} else if (status === 'failed') {
console.error(`Job ${jobId} failed: ${error}`);
}
});

app.listen(3000);

Submit the job with callbackUrl pointing to your endpoint — no polling needed.


Next steps