Editframe API & CLI
@editframe/api is a JavaScript/TypeScript client for Editframe's video rendering API. The editframe CLI adds local rendering and preview during development. Use these tools to render videos from HTML compositions, upload and process media files, transcribe audio, and manage authenticated browser access to CDN resources.
Quick Start
import { Client, createRender, getRenderProgress, downloadRender } 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>`,
width: 1920,
height: 1080,
fps: 30,
});
for await (const event of await getRenderProgress(client, render.id)) {
if (event.type === "progress") console.log(`Progress: ${(event.data.progress * 100).toFixed(0)}%`);
}
const response = await downloadRender(client, render.id);
const buffer = await response.arrayBuffer();
# Local dev: preview and render with the CLI instead of the API
npx editframe preview
npx editframe render -o output.mp4
Client & Authentication
import { Client } from "@editframe/api";
const client = new Client(process.env.EDITFRAME_API_KEY); // server-side: Bearer token
const client = new Client(); // browser: session cookie (credentials: "include")
const client = new Client(apiKey, "https://staging.editframe.com"); // optional second arg overrides the host (default https://editframe.com)
An API key has the shape ef_<secret>_<keyid>. The client validates this shape locally. Get a key from the Editframe dashboard, under Settings → API Keys. Every SDK function throws on a non-OK response, with the HTTP status and the response body in the error message.
Never expose an API key in client-side code. For browser playback of authenticated media, use URL Signing instead. The server holds the key. The browser receives only short-lived, scoped tokens.
Function Index
Renders
createRender(client, payload)→CreateRenderResult— Create a render job from HTML compositionuploadRender(client, renderId, fileStream)→Promise<void>— Upload a pre-rendered video file instead of rendering from HTMLgetRenderProgress(client, id)→CompletionIterator— Stream render progress via SSEgetRenderInfo(client, id)→LookupRenderByMd5Result— Get render metadata (status,expires_at, error)lookupRenderByMd5(client, md5)→LookupRenderByMd5Result | null— Find existing render by hashdownloadRender(client, id)→Response— Download completed render (always fetches/api/v1/renders/:id.mp4regardless of output container — check the output config you passed tocreateRender, or the response'sContent-Type, to know the actual format)deleteRender(client, id)→{ success: boolean }— Delete a render and all GCS output immediately; fails while status isqueued/rendering/recovering
Files (unified API — video, image, caption)
createFile(client, payload)→CreateFileResult— Register a file recorduploadFile(client, uploadDetails, fileStream)→IteratorWithPromise<UploadChunkEvent>— Upload file content, chunked + resumablegetFileDetail(client, id)→FileDetail— Metadata; video files also includetrackslookupFileByMd5(client, md5)→LookupFileByMd5Result | null— Find existing file by hashdeleteFile(client, id)→{ success: boolean }getFileProcessingProgress(client, id)→ProgressIterator— SSE stream of ISOBMFF processing progress (video only)transcribeFile(client, id, options?)→TranscribeFileResult— Start transcription (options.trackIddefaults to the first audio track)getFileTranscription(client, id)→FileTranscriptionResult | nullcreateFileTrack/uploadFileTrack/uploadFileIndex— Lower-level track/index upload, for pre-processed ISOBMFF from an external transcoder (CLI'scloud-renderuses these internally)
@editframe/api still exports older, type-specific resources: createImageFile, createCaptionFile, createISOBMFFFile, createTranscription, and others. These are @deprecated in favor of the unified createFile/type: "video"|"image"|"caption" API above. Do not use them in new code.
Node.js helper
upload(client, filePath)(from@editframe/api/node) →{ file, uploadIterator }— Auto-detects type from extension, computes MD5, handles chunked upload
URL Signing
createURLToken(client, url)→string— Generate a signed JWT for browser access to a specific media URL
Unified Files API
All file types — video, image, caption — share one set of endpoints. A type field selects the endpoint's behavior:
| Type | Formats | Max size | Processing |
|---|---|---|---|
video |
MP4, MOV, WebM, MKV | 1GB | Auto-converted to ISOBMFF (enables frame-accurate seek + adaptive streaming) |
image |
JPEG, PNG, WebP, SVG | 16MB | Ready immediately |
caption |
VTT, SRT | 2MB | Ready immediately |
Status lifecycle: created → uploading → processing → ready (image/caption skip processing), or → failed.
import { Client, createFile, uploadFile, getFileProcessingProgress } from "@editframe/api";
import { createReadStream } from "node:fs";
import { stat } from "node:fs/promises";
const client = new Client(process.env.EDITFRAME_API_KEY);
const fileStats = await stat("video.mp4");
const file = await createFile(client, { filename: "video.mp4", type: "video", byte_size: fileStats.size });
for await (const event of uploadFile(client, { id: file.id, byte_size: fileStats.size, type: "video" }, createReadStream("video.mp4"))) {
if (event.type === "progress") console.log(`Upload: ${(event.progress * 100).toFixed(1)}%`);
}
for await (const event of await getFileProcessingProgress(client, file.id)) {
if (event.type === "complete") break;
}
In Node.js, do this with one call instead: const { file, uploadIterator } = await upload(client, "video.mp4");, from @editframe/api/node.
Check for an existing file before you upload it. Call lookupFileByMd5(client, md5). This returns null when no file matches.
Use the file in a composition through file-id:
<ef-configuration api-host="https://editframe.com">
<ef-video file-id="uuid-of-processed-video"></ef-video>
<ef-image file-id="uuid-of-uploaded-image" class="w-24 h-24"></ef-image>
</ef-configuration>
createFile assigns file-id as a stable UUID. It stays the same through upload, processing, and playback.
Retention (expires_at)
createFile and createRender both accept an optional expires_at: an ISO 8601 date, at most 30 days in the future. Omit it for permanent retention. Invalid input returns 400, with one of invalid_datetime, must_be_future, or exceeds_max_retention. getFileDetail and getRenderInfo echo expires_at back; null means permanent. Remote-URL render ingest (see below) uses a fixed, non-configurable one-hour TTL, unrelated to this field.
Renders
const render = await createRender(client, {
html: `<ef-timegroup mode="contain" class="w-[1920px] h-[1080px]"><ef-video src="..."></ef-video></ef-timegroup>`,
width: 1920,
height: 1080,
fps: 30, // default 30
output: { container: "mp4", video: { codec: "h264" }, audio: { codec: "aac" } }, // default shown
});
Output containers: mp4 (video.codec: "h264", audio.codec: "aac"), jpeg (quality 1–100, default 80), png (compression 1–100 default 80, transparency boolean), webp (quality 1–100 default 80, compression 0–6 default 4, transparency boolean).
createRender also accepts these advanced options:
backend:"cpu"or"gpu". Defaults to"cpu".work_slice_ms: splits the render into fragments of this length, in milliseconds. Defaults to 4000 on"cpu", or 15000 on"gpu". Maximum 60000.strategy: reserved for future render strategies. Only"v1"exists today, and it is the default.duration_ms: overrides the render's detected duration, in milliseconds.metadata: an arbitraryRecord<string, string>.getRenderInfoand render webhooks echo it back.
A composition's ef-video, ef-audio, and ef-image elements can use https:// src values directly. Editframe downloads and ingests these before rendering, on an ephemeral, roughly one-hour expires_at. This differs from a file you register with createFile.
Pass md5 to skip duplicate work. When a render with that hash already exists, createRender returns it instead of rendering again:
import { md5 } from "@editframe/assets";
const render = await createRender(client, { md5: md5(html), html, width: 1920, height: 1080 });
getRenderProgress yields { type: "progress", data: { progress } }, with progress from 0 to 1, then yields { type: "complete" }. It throws if the render fails. deleteRender removes the row and all GCS output and intermediates immediately. It fails while the render is still active: queued, rendering, or recovering.
To register a video you rendered yourself, skip html and upload the file directly:
const render = await createRender(client, { width: 1920, height: 1080, fps: 30 });
await uploadRender(client, render.id, createReadStream("my-video.mp4"));
Transcription
const transcription = await transcribeFile(client, file.id, { trackId: 1 }); // trackId optional, defaults to first audio track
const result = await getFileTranscription(client, file.id); // null if none exists yet; result.status === "completed" when done
Load the transcript into <ef-captions captions-src="captions.json" target="ef-video-id">. target only syncs caption timing to the referenced ef-video/ef-audio element's local time. It does not fetch transcription data by itself. Set captions-src, captions-script, or captions.captionsData to supply the actual segments. See the composition skill's ef-captions reference.
URL Signing
Use URL signing whenever a browser plays Editframe-hosted media directly, for example <ef-video src="https://editframe.com/api/v1/transcode/...">. Skip it when all rendering and playback happens server-side, through createRender/downloadRender.
Your server holds the API key. It exposes an endpoint that calls createURLToken. The frontend points <ef-configuration signingURL="..."> at that endpoint. A media element detects an authenticated URL, then sends a POST request to signingURL with { url }. It attaches the returned JWT as Authorization: Bearer <token> on the actual media request.
Tokens last roughly one hour. The browser caches each token per URL. For a transcode or HLS endpoint, one token covers the manifest and all its segments.
// server
import { Client, createURLToken } from "@editframe/api";
const client = new Client(process.env.EDITFRAME_API_KEY);
app.post("/sign-url", async (req, res) => res.json({ token: await createURLToken(client, req.body.url) }));
<!-- frontend -->
<ef-configuration signingURL="/sign-url"></ef-configuration>
<ef-video src="https://editframe.com/api/v1/transcode/manifest.m3u8?url=..."></ef-video>
If you rely on editframe.com session cookies instead of an API key, use POST /ef-sign-url instead, with credentials: "include" and no Authorization header. This is the equivalent anonymous-token flow. Most apps use the API-key server route above instead.
CLI
npx @editframe/cli <command> # one-off
npm install -g @editframe/cli # or install globally
# already included in every scaffolded project (npm start → editframe preview)
Global options: -t, --token <token> (or EF_TOKEN env var), --ef-host <host> (default https://editframe.com), --ef-render-host <host> (or EF_RENDER_HOST env var, default https://editframe.com; sets where editframe render posts local telemetry), -V/--version, -h/--help.
| Command | Purpose |
|---|---|
editframe preview [directory] |
Starts a Vite dev server + opens the project (. by default) with HMR. |
editframe render [directory|--url <url>] [options] |
Renders locally via headless Chrome + ffmpeg. See options below. |
editframe transcribe <input> [-o file] [-l lang] |
Generates word-level captions via whisper_timestamped (default output captions.json). Requires pip3 install whisper-timestamped. The -l/--language flag is currently a no-op; transcription always runs in English. |
editframe cloud-render [directory] [-s v1] |
Builds with Vite, syncs assets, submits a render job to Editframe's cloud. Requires a token. |
editframe sync |
Syncs assets from src/assets/.cache to Editframe servers (a prerequisite step cloud-render also runs itself). |
editframe process [directory] |
Builds and processes assets for cloud rendering without submitting a render. |
editframe process-file <file> |
Uploads a single audio/video file for processing, with progress. |
editframe auth |
Shows the API key's name/organization (needs --token/EF_TOKEN). |
editframe check |
Verifies ffmpeg and whisper_timestamped are installed; prints platform-specific install instructions if not. |
editframe webhook [-t topic] |
Sends a test webhook event to the URL already configured on your API key (there is no CLI flag to pass an ad-hoc URL). Prompts interactively for topic if omitted. Topics: render.created, render.pending, render.rendering, render.completed, render.failed. |
editframe mux <path> |
Probes a media file and prints its audio/video track info. |
render options
| Option | Default | Description |
|---|---|---|
-o, --output <path> |
output.mp4 |
Output file path |
--url <url> |
— | Render an already-running URL instead of starting a Vite server for [directory] |
-d, --data <json> / --data-file <path> |
— | Custom render data, read in-page via getRenderData() from @editframe/elements |
--fps <number> |
30 |
|
--scale <number> |
1 |
Resolution scale, 0–1 |
--include-audio / --no-include-audio |
include | |
--from-ms / --to-ms <number> |
— | Render a sub-range |
--experimental-native-render |
off | Canvas-capture API path, faster |
--profile / --profile-output <path> |
off / ./render-profile.cpuprofile |
CPU profiling |
// Read --data / --data-file inside the composition:
import { getRenderData } from "@editframe/elements";
const data = getRenderData<{ userName: string }>();
if (data) document.querySelector("#name").textContent = data.userName;
The transcription output, captions.json, matches <ef-captions captions-src>'s expected shape: segments/word_segments, with millisecond timestamps. See the composition skill's ef-captions reference for styling, for example the <ef-captions-active-word> nested child.