Renders

Create render jobs, track progress, and download the output.

Create a render

import { Client, createRender } from "@editframe/api";

const client = new Client(process.env.EDITFRAME_API_KEY);

const render = await createRender(client, {
  html: `<ef-timegroup mode="contain" class="w-[1920px] h-[1080px]">
    <ef-video src="https://assets.editframe.com/bars-n-tone.mp4"></ef-video>
  </ef-timegroup>`,
});
// { id: "b3d2f1c0-…", md5: null, status: "created", metadata: {} }

Pass your composition as an inline HTML string. The composition runs in a headless browser; the resulting video is encoded and stored. To render a local project, use the cloud-render CLI command instead.

Asset ingest: ef-video, ef-audio, and ef-image elements with https:// sources are automatically downloaded, processed, and stored by the server before rendering begins. Relative or local src paths are not supported in inline HTML renders.

POST https://editframe.com/api/v1/renders

Request body

Width, height, fps, and duration are extracted from the composition automatically — you only need to supply them if you want to override what the composition declares.

FieldTypeDescriptionDefault
htmlstringComposition HTML string
widthnumberOutput width in pixelsfrom composition
heightnumberOutput height in pixelsfrom composition
fpsnumberFrames per second (1–120)from composition
duration_msnumberDuration in millisecondsfrom composition
outputobjectOutput format (see below)mp4 / h264 / aac
metadataobjectArbitrary string → string key-value pairs{}

Output formats

The output field selects the container and configures encoding. Omit it entirely to get the default MP4 output.

MP4

The only supported video container. Codec values are fixed — "h264" for video and "aac" for audio are the only accepted options.

output: {
  container: "mp4",
  video: { codec: "h264" },
  audio: { codec: "aac" },
}

Still images

Still renders capture a single frame at duration_ms. Three formats are supported.

JPEG — lossy compression, best for photographic content:

output: {
  container: "jpeg",
  quality: 80, // 1–100; higher = better quality, larger file. Default: 80
}

PNG — lossless, suitable when transparency or pixel-perfect output is needed:

output: {
  container: "png",
  compression: 80,   // 1–100; higher = smaller file, slower encode. Default: 80
  transparency: false, // true to preserve the alpha channel. Default: false
}

WebP — modern format with good quality-to-size ratio, supports transparency:

output: {
  container: "webp",
  quality: 80,       // 1–100; controls lossy quality. Default: 80
  compression: 4,    // 0–6; encoding effort (0 = fastest, 6 = smallest file). Default: 4
  transparency: false, // true to preserve the alpha channel. Default: false
}

Bundle upload workflow

For compositions that can't be expressed as a single HTML string — for example, a Vite project with local JS modules — use the two-step bundle upload workflow.

1. Create the render

Call createRender without the html field:

import { createRender, Client } from "@editframe/api";

const client = new Client(process.env.EDITFRAME_API_KEY);

const render = await createRender(client, {});
// { id: "b3d2f1c0-…", status: "created" }

2. Upload the bundle

bundleRender runs a Vite build of your project and returns a ready-to-upload tar stream:

import { uploadRender } from "@editframe/api";
import { bundleRender } from "@editframe/api/resources/renders.bundle";

const tarStream = await bundleRender({ root: "./src", renderData: {} });
await uploadRender(client, render.id, tarStream);

root is the directory containing your index.html and vite.config. renderData is injected as the RENDER_DATA global into the build — pass {} if your composition doesn't use it.

After upload the render transitions to queued and then into the normal rendering pipeline. Progress and download work identically to HTML renders.

Asset references: For best results, ef-video, ef-image, and ef-audio elements should use file-id attributes pointing to pre-uploaded Editframe files — these are fully processed and stored on Editframe's infrastructure before the render starts. https:// URLs also work: the renderer will byte-range read from the original URL at render time, but this is less reliable than pre-uploading.

Bundle format requirements

If you're building the bundle yourself, uploadRender expects a gzip tar stream with index.html at the tarball root (not inside a subdirectory). The build output must be a single self-contained HTML file — all JS and CSS inlined. Using the tar npm package:

import { uploadRender, createReadableStreamFromReadable } from "@editframe/api/node";
import * as tar from "tar";
import { PassThrough } from "node:stream";

// dist/ must contain index.html at its root with all assets inlined
const tarStream = tar.create({ gzip: true, cwd: "/path/to/dist" }, ["."]);
const pass = new PassThrough();
tarStream.pipe(pass);

await uploadRender(client, render.id, createReadableStreamFromReadable(pass));

Response

FieldTypeDescription
idstringUUID for this render job
md5string | nullContent hash, if provided at create time
statusstringAlways "created" on successful create
metadataobjectKey-value metadata passed at create time

Status values

StatusDescription
createdJob created; HTML renders begin processing immediately
queuedBundle uploaded and waiting to initialize
renderingFrames are being captured and encoded
completeRender finished; download is available
failedRender failed

Track progress

getRenderProgress opens a Server-Sent Events stream at GET /api/v1/renders/:id/progress and returns an async iterator:

import { getRenderProgress } from "@editframe/api";

const progress = await getRenderProgress(client, render.id);

for await (const event of progress) {
  if (event.type === "progress") {
    console.log(`${Math.round(event.data.progress * 100)}%`);
  }
}
// Stream closes automatically when rendering is complete

event.data.progress is a 0–1 fraction. The iterator throws on error.

If you prefer to poll, GET /api/v1/renders/:id returns the current status and is safe to call on an interval.

Get render info

import { getRenderInfo } from "@editframe/api";

const render = await getRenderInfo(client, renderId);

GET https://editframe.com/api/v1/renders/:id

{
  "id": "b3d2f1c0-…",
  "status": "complete",
  "fps": 30,
  "width": 1920,
  "height": 1080,
  "duration_ms": 10000,
  "created_at": "2026-05-08T00:00:00Z",
  "completed_at": "2026-05-08T00:01:30Z",
  "failed_at": null
}

Download

import { downloadRender } from "@editframe/api";

const response = await downloadRender(client, render.id);
const buffer = await response.arrayBuffer();

downloadRender fetches GET /api/v1/renders/:id.mp4. For still image renders, use client.authenticatedFetch with the matching extension:

const response = await client.authenticatedFetch(`/api/v1/renders/${render.id}.jpeg`);
ContainerURL
mp4/api/v1/renders/:id.mp4
jpeg/api/v1/renders/:id.jpeg
png/api/v1/renders/:id.png
webp/api/v1/renders/:id.webp

Webhooks

For long jobs, use webhooks to receive a render.completed event instead of polling.