Video Editor API: A Developer's Guide to Programmatic Video
Render videos programmatically with the iLoveVideoEditor REST API: VideoJSON, the Node SDK, template variables, webhooks, and MCP tools.
Programmatic video usually means one of three things: wrestling an FFmpeg pipeline into production, paying a cloud render service per-minute fees that scale badly, or giving up and exporting by hand. The iLoveVideoEditor API is a fourth option: describe the video as JSON, POST it, poll or receive a webhook, download an MP4.
This guide walks the whole flow with copy-paste code: the REST API, the official Node SDK, the VideoJSON scene format, rendering templates with variables, signed webhooks, error handling, and driving the same pipeline from an AI agent over MCP.
What you're building against
- Base URL:
https://api.ilovevideoeditor.com— every route below is prefixed with/v1. - Auth: an API key from the dashboard, sent as the
x-api-keyheader. JWT bearer tokens work too, but the key is what you want for server-side jobs. - Cost model: renders consume credits. As a rule of thumb, 1 credit covers about a minute of 1080p30 output, and you can estimate the cost of any payload before spending anything — see the pricing section.
Everything the browser editor can do is expressible through the API, because the editor itself produces the same VideoJSON the API consumes.
Quick start with the Node SDK
The official SDK is a zero-dependency TypeScript client that runs anywhere fetch exists (Node 18+, Deno, Bun, Cloudflare Workers). Heads up: the package is preparing for its npm release — until it lands, everything in this guide works with plain fetch against the REST endpoints, and each SDK example names the route it calls so translating it is a one-liner.
import { ILoveVideoEditorClient } from '@ilovevideoeditor/sdk-node';
const client = new ILoveVideoEditorClient({
apiKey: process.env.VF_API_KEY!,
// baseUrl defaults to https://api.ilovevideoeditor.com
});
const result = await client.render(videoJSON, {
pollIntervalMs: 2000, // how often to poll (default 2s)
maxPollTimeMs: 300_000, // give up after 5 minutes (default)
onProgress: ({ status, progress }) => {
console.log(status + ' — ' + progress + '%');
},
});
if (result.status === 'completed') {
console.log('Download URL:', result.downloadUrl);
} else {
console.error('Render failed:', result.error);
}client.render() queues the job and polls until it reaches a terminal state, then refreshes the download URL for you. To control the loop yourself — for example inside a queue worker — use the two-step version:
const { jobId } = await client.queueRender(videoJSON); // POST /v1/render
const status = await client.getRender(jobId); // GET /v1/render/:idThe RenderResult shape: jobId, status ('pending' | 'rendering' | 'completed' | 'failed'), progress, url, downloadUrl, error, createdAt, completedAt. When a completed render's URL expires, call client.refreshUrl(jobId) (or POST /v1/render/:id/refresh-url) for a fresh one — you don't need to re-render.
VideoJSON: the scene format
VideoJSON is a declarative scene graph. The rules that matter:
- The document is
{ name, layers }. The first layer must be a `composition` whosesettingsdefine the canvas:width(max 7680),height(max 4320),fps(max 120), andsourceDuration— the total length in seconds (max 3600). - Every layer has
id,type,settings,properties, andanimations.settingscarries timing:startTime(when the layer appears on the timeline),sourceDuration(how long it plays, in source seconds), plus media fields likesourcefor video, image, and audio layers. propertiesholds static values.positionandanchorare normalized[x, y]arrays —[0.5, 0.5]is dead center,[0, 0]is top-left.- Anything that changes over time goes in
animationsas keyframes. Valid easing values arestep,linear,easeIn,easeOut, andeaseInOut(a missing easing meanslinear). - GLSL effects attach as
{ effect, params }entries and run in array order;transitionIn/transitionOutname a transition preset with a duration in seconds. - Limits: up to 50 layers, group nesting depth 3.
A complete, valid example — a 3-second 1080p30 clip with a background video, a vignette effect, a fade-in transition, and an animated title:
{
"name": "hello-api",
"version": 2,
"layers": [
{
"id": "root",
"type": "composition",
"settings": {
"enabled": true,
"startTime": 0,
"sourceDuration": 3,
"width": 1920,
"height": 1080,
"fps": 30,
"backgroundColor": "#0f0f12"
},
"properties": {},
"animations": []
},
{
"id": "bg",
"type": "video",
"settings": {
"enabled": true,
"startTime": 0,
"sourceDuration": 3,
"name": "Background",
"source": "https://example.com/clip.mp4"
},
"properties": {
"fit": "cover",
"position": [0.5, 0.5],
"scale": 1,
"opacity": 1
},
"animations": [],
"transitionIn": { "transition": "fade", "duration": 0.5 },
"effects": [
{
"effect": "vignette",
"params": { "intensity": 0.6, "radius": 0.9, "softness": 0.4 }
}
]
},
{
"id": "title",
"type": "text",
"settings": {
"enabled": true,
"startTime": 0,
"sourceDuration": 3,
"name": "Title"
},
"properties": {
"text": "Hello from the API",
"fontFamily": "Noto Sans",
"fontSize": 96,
"fontWeight": 700,
"color": "#FFFFFF",
"position": [0.5, 0.5]
},
"animations": [
{
"id": "title-fade",
"property": "opacity",
"keyframes": [
{ "time": 0, "value": 0, "easing": "easeOut" },
{ "time": 0.5, "value": 1 }
]
}
]
}
]
}The full catalog of what you can put in effects, transitionIn, and animations lives in the docs: 43 GLSL effects, 27 transitions, and the animation presets.
You don't have to hand-write any of this: build the scene visually in the studio, then copy the project's VideoJSON. The editor is the reference implementation.
Render a template with variables
Hand-rolling VideoJSON makes sense when every video is bespoke. When your videos share a structure — product launches, real-estate reels, quote cards — use a template. The library ships 249 templates, each with a typed variablesSchema.
The SDK lists and fetches templates:
const { templates } = await client.listTemplates(); // GET /v1/templates
const { template } = await client.getTemplate('ecommerce-product-launch');
console.log(template.variablesSchema);
// [{ key: 'productName', type: 'text', required: true, ... }, ...]Rendering a template is one REST call — POST /v1/templates/:id/render with the variable values. The SDK doesn't wrap this endpoint yet, so use fetch:
const res = await fetch(
'https://api.ilovevideoeditor.com/v1/templates/ecommerce-product-launch/render',
{
method: 'POST',
headers: {
'x-api-key': process.env.VF_API_KEY!,
'Content-Type': 'application/json',
},
body: JSON.stringify({
variables: {
productName: 'Aurora Headphones',
tagline: 'Sound without compromise',
price: '€199',
cta: 'Shop now',
},
webhookUrl: 'https://api.example.com/render-done', // optional
}),
},
);
if (!res.ok) {
const body = await res.json();
throw new Error('Template render failed: ' + JSON.stringify(body));
}
const { jobId } = await res.json();The server merges your variables with the template's defaults and returns 400 when a required variable is missing — the error body is { error: 'Missing required template variables', details: [...] }. From there the job behaves exactly like a raw render: same status endpoint, same webhooks.
Polling vs webhooks
For interactive flows, polling with client.render() is fine. For production pipelines — where a render might take minutes and your HTTP handler shouldn't sit open — subscribe to webhooks once and let the API call you:
const res = await fetch('https://api.ilovevideoeditor.com/v1/webhooks', {
method: 'POST',
headers: {
'x-api-key': process.env.VF_API_KEY!,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://api.example.com/ilve-webhook',
events: ['render.completed', 'render.failed'],
}),
});
const subscription = await res.json();
// { id, url, events, secret, createdAt } — store `secret`; it verifies every deliveryTwo things to know:
- You can have up to 10 active subscriptions. Manage them with
GET /v1/webhooksandDELETE /v1/webhooks/:id. - There's also a per-job alternative: pass
webhookUrlin thePOST /v1/render(or template render) body. Useful for one-off jobs whose result routes into a specific workflow.
The payload for a completed render looks like this (a failed render carries event: 'render.failed' and an error message):
{
"event": "render.completed",
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "completed",
"stage": "done",
"progress": 100,
"url": "https://cdn.ilovevideoeditor.com/renders/a1b2c3d4.mp4",
"duration": 12.5,
"width": 1920,
"height": 1080,
"cost": 3,
"error": null,
"createdAt": "2026-01-15T10:00:00.000Z",
"completedAt": "2026-01-15T10:01:30.000Z"
}Every delivery is signed. Verify the X-ILVE-Signature header (format t=<unix seconds>,v1=<HMAC-SHA256 hex of "t.body">) against the raw request body before trusting anything:
import { createHmac, timingSafeEqual } from 'node:crypto';
function verifySignature(
rawBody: string,
header: string,
secret: string,
): boolean {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const expected = createHmac('sha256', secret)
.update(parts.t + '.' + rawBody)
.digest('hex');
const a = Buffer.from(parts.v1 ?? '', 'hex');
const b = Buffer.from(expected, 'hex');
return a.length === b.length && timingSafeEqual(a, b);
}Failed deliveries are retried up to 3 times (after roughly 5s, 15s, and 45s), so make your handler idempotent on the render id — the same event can arrive more than once. One honest limitation: webhooks fire only on terminal states. There are no progress webhooks, so keep polling if your UI needs a progress bar.
Handling errors
The SDK throws ILoveVideoEditorError with statusCode and the parsed responseBody, which makes failures easy to branch on:
import {
ILoveVideoEditorClient,
ILoveVideoEditorError,
} from '@ilovevideoeditor/sdk-node';
try {
const result = await client.render(videoJSON);
console.log(result.downloadUrl);
} catch (err) {
if (err instanceof ILoveVideoEditorError) {
switch (err.statusCode) {
case 400: // invalid videoJSON — err.responseBody.details has field errors
case 401: // missing or invalid API key
case 404: // job not found (or not yours)
default:
console.error(err.statusCode, err.message, err.responseBody);
}
}
throw err;
}A few behaviors worth building around:
- Validation happens at submission. A malformed VideoJSON is rejected with
400before it consumes credits, so fail fast and logdetails. - `render()` has its own timeout (
maxPollTimeMs, default 5 minutes). Long videos will outgrow it — either raise it or switch toqueueRenderplus webhooks. - Failed renders refund their credits automatically, and
result.errortells you why — an unreachablesourceURL is the most common cause in practice.
Driving renders with AI agents (MCP)
If video generation is triggered by an LLM — support agents producing personalized replies, ops turning spreadsheet rows into social clips — skip the glue code entirely. The @ilovevideoeditor/mcp-server package exposes the render pipeline as Model Context Protocol tools, so any MCP-capable client (Claude, Cursor, or your own agent runtime) can list templates, render, and fetch results from a prompt. Like the SDK, the MCP server package is preparing for its npm release — the configuration below is what you'll add to your MCP client once it lands.
It reads your API key from VF_API_KEY (and optionally VF_API_BASE_URL):
{
"mcpServers": {
"ilovevideoeditor": {
"command": "npx",
"args": ["-y", "@ilovevideoeditor/mcp-server"],
"env": { "VF_API_KEY": "vf_live_..." }
}
}
}The tools the agent gets:
| Tool | What it does |
|---|---|
ilovevideoeditor_list_templates | List all templates with their variable schemas |
ilovevideoeditor_get_template | Fetch one template by ID |
ilovevideoeditor_get_layer_capabilities | Describe a layer type's properties, effects, and transitions |
ilovevideoeditor_render_json | Queue a raw VideoJSON render |
ilovevideoeditor_render_template | Compile a template with variables and queue it |
ilovevideoeditor_get_render_status | Poll a job by ID |
ilovevideoeditor_get_download_url | Get a fresh download URL for a completed job |
ilovevideoeditor_local_preview | Open a local preview of the composition |
ilovevideoeditor_local_capture | Capture a frame as PNG or JPEG |
ilovevideoeditor_local_analyze | Analyze a rendered frame |
The local_* tools are the underrated part: the agent previews the composition and inspects captured frames locally before spending a single credit on a cloud render.
What it costs
Rendering is metered in credits. Pro is €25/month for 100 credits, Business is €50/month for 250, and one-time top-up packages start at 800 credits and never expire. Roughly, 1 credit buys a minute of 1080p30 output — see the pricing page for the current tiers.
Don't guess costs: POST /v1/render/cost with your VideoJSON returns the exact credit cost, estimated duration, resolution, and fps without consuming anything. Wire it into your UI so users see the price before they commit:
const res = await fetch('https://api.ilovevideoeditor.com/v1/render/cost', {
method: 'POST',
headers: {
'x-api-key': process.env.VF_API_KEY!,
'Content-Type': 'application/json',
},
body: JSON.stringify({ videoJSON }),
});
const { cost, estimatedDuration, resolution, fps } = await res.json();
// cost: the credits this render will consumeWhere to go next
- Browse the 249 templates and their variable schemas
- Full reference for effects, transitions, and animations
- The developer solutions page for architecture guidance
- Video editing workflow automation — putting renders behind triggers and queues
- Open the studio to build scenes visually, then copy the VideoJSON
Start small: one template, one webhook, one render. The JSON the API accepts is the same JSON the editor produces, so everything you prototype visually is one POST away from production.
Try it yourself — free, in your browser
No install, no upload, no credit card. Open the studio or start from one of 259 motion-designed templates.
Related posts
How to Trim Videos Online Without Uploading Them
Trim videos directly in your browser — no upload, no server, no waiting. Learn the workflow, the real keyboard shortcuts, and the honest limits.
Read moreHow to Add Text to Video: The Complete Guide
Titles, lower thirds, captions, and animated text — the complete workflow for adding text to video in your browser, with honest tool notes.
Read moreHow to Add Captions to Video: The Fastest Method
The practical caption workflow: generate an SRT anywhere, then import, style, and burn it in — for YouTube, TikTok, Instagram, and more.
Read more