# Editframe API

> JavaScript/TypeScript SDK and CLI for Editframe's video rendering API. Create renders, upload and process video, image, and caption files, transcribe audio, sign URLs for browser playback, and render or preview compositions from the command line.

- Skill: `editframe/editframe-api` (Agent Skill)
- Install (CLI): `npx skillmds@latest add editframe/editframe-api`
- Raw SKILL.md: https://api.skillmd.com/api/skills/editframe/editframe-api/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- License: MIT
- Author: editframe (https://skillmd.com/u/editframe)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/editframe/editframe-api

---



# 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

```typescript
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();
```

```bash
# Local dev: preview and render with the CLI instead of the API
npx editframe preview
npx editframe render -o output.mp4
```

## Client & Authentication

```typescript
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](#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 composition
- `uploadRender(client, renderId, fileStream)` → `Promise<void>` — Upload a pre-rendered video file instead of rendering from HTML
- `getRenderProgress(client, id)` → `CompletionIterator` — Stream render progress via SSE
- `getRenderInfo(client, id)` → `LookupRenderByMd5Result` — Get render metadata (status, `expires_at`, error)
- `lookupRenderByMd5(client, md5)` → `LookupRenderByMd5Result | null` — Find existing render by hash
- `downloadRender(client, id)` → `Response` — Download completed render (always fetches `/api/v1/renders/:id.mp4` regardless of output container — check the output config you passed to `createRender`, or the response's `Content-Type`, to know the actual format)
- `deleteRender(client, id)` → `{ success: boolean }` — Delete a render and all GCS output immediately; fails while status is `queued`/`rendering`/`recovering`

### Files (unified API — video, image, caption)
- `createFile(client, payload)` → `CreateFileResult` — Register a file record
- `uploadFile(client, uploadDetails, fileStream)` → `IteratorWithPromise<UploadChunkEvent>` — Upload file content, chunked + resumable
- `getFileDetail(client, id)` → `FileDetail` — Metadata; video files also include `tracks`
- `lookupFileByMd5(client, md5)` → `LookupFileByMd5Result | null` — Find existing file by hash
- `deleteFile(client, id)` → `{ success: boolean }`
- `getFileProcessingProgress(client, id)` → `ProgressIterator` — SSE stream of ISOBMFF processing progress (video only)
- `transcribeFile(client, id, options?)` → `TranscribeFileResult` — Start transcription (`options.trackId` defaults to the first audio track)
- `getFileTranscription(client, id)` → `FileTranscriptionResult | null`
- `createFileTrack` / `uploadFileTrack` / `uploadFileIndex` — Lower-level track/index upload, for pre-processed ISOBMFF from an external transcoder (CLI's `cloud-render` uses 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`.

```typescript
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`:

```html
<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

```typescript
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 arbitrary `Record<string, string>`. `getRenderInfo` and 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:

```typescript
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:

```typescript
const render = await createRender(client, { width: 1920, height: 1080, fps: 30 });
await uploadRender(client, render.id, createReadStream("my-video.mp4"));
```

## Transcription

```typescript
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.

```typescript
// 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) }));
```

```html
<!-- 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

```bash
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 |

```typescript
// 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.

