Files

Upload video, image, and caption files for use in compositions and renders.

Files are uploaded in two steps: first register the file to get an ID, then stream the bytes using the SDK's chunked uploader.

Upload a file

Use the @editframe/api SDK to handle both steps:

js
import { createFile, uploadFile } from "@editframe/api";
import { createReadStream, statSync } from "node:fs";

const filePath = "clip.mp4";
const byteSize = statSync(filePath).size;

// Step 1: Register the file and get an ID
const file = await createFile(client, {
  filename: "clip.mp4",
  type: "video",
  byte_size: byteSize,
  mime_type: "video/mp4",
});

// Step 2: Stream the bytes with chunked upload
for await (const event of uploadFile(
  client,
  { id: file.id, byte_size: byteSize, type: file.type },
  createReadStream(filePath),
)) {
  if (event.type === "progress") {
    console.log(`Uploading: ${Math.round(event.progress * 100)}%`);
  }
}

console.log(file.id); // stable UUID — use this everywhere

Uploads are resumable. If the connection drops, re-running uploadFile with the same id resumes from where it left off.

Node.js shorthand

For simple cases, the upload helper does everything in one call:

js
import { upload } from "@editframe/api/node";

const { file, uploadIterator } = await upload(client, "clip.mp4");

for await (const event of uploadIterator) {
  if (event.type === "progress") {
    console.log(`Uploading: ${Math.round(event.progress * 100)}%`);
  }
}

This auto-detects the file type from the extension and handles chunked transfer automatically.

Create a file from a URL

A video that you already host at an HTTPS URL can become a file without an upload. Editframe reads it in place and does not copy it: pipeline steps read the byte ranges they need from your URL.

js
import { createFile } from "@editframe/api";

const file = await createFile(client, {
  type: "video",
  source_url: "https://media.example.com/fleet/truck-1182.mp4",
});

console.log(file.status); // "ready"
console.log(file.source_url); // the URL after redirects

byte_size, md5 and mime_type are not allowed with source_url. Editframe checks the URL when you create the file, and the response is the file with these values:

A URL that cannot be read by byte range returns 422, with a code such as source_range_unsupported as error and the reason as message. See URL errors for every code. The source must not change, and must stay readable while you use the file.

Wait for processing

Video files are transcoded after upload. Poll getFileDetail until status is "ready":

js
import { getFileDetail } from "@editframe/api";

async function waitForFile(client, id) {
  while (true) {
    const detail = await getFileDetail(client, id);

    if (detail.status === "ready") return detail;
    if (detail.status === "failed") throw new Error(`Processing failed: ${id}`);

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

const ready = await waitForFile(client, file.id);

Image and caption files skip transcoding and are "ready" immediately after upload completes. A file created from a URL is "ready" when it is created.

Retention and expiration

By default, uploaded files are retained until you delete them. The expires_at field on the file record is null, which means the file is not scheduled for automatic removal.

Setting a TTL at upload time

Pass expires_at as an ISO 8601 datetime when registering the file. The maximum allowed value is 30 days in the future. TTL can only be set at creation time — there is no API to change expires_at on an existing file.

js
import { createFile, uploadFile, getFileDetail } from "@editframe/api";
import { createReadStream, statSync } from "node:fs";

const filePath = "clip.mp4";
const byteSize = statSync(filePath).size;

// Expire in 7 days
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);

const file = await createFile(client, {
  filename: "clip.mp4",
  type: "video",
  byte_size: byteSize,
  mime_type: "video/mp4",
  expires_at: expiresAt.toISOString(), // ISO 8601, max 30 days ahead
});

for await (const event of uploadFile(
  client,
  { id: file.id, byte_size: byteSize, type: file.type },
  createReadStream(filePath),
)) {
  if (event.type === "progress") {
    console.log(`Uploading: ${Math.round(event.progress * 100)}%`);
  }
}

const detail = await getFileDetail(client, file.id);
console.log(detail.expires_at); // "2026-06-22T12:00:00.000Z"

expires_at is validated on every call to createFile. Omitting the field, or passing null, means permanent retention. Any other value must be a future ISO 8601 datetime no more than 30 days out, or POST /api/v1/files returns 400 with one of:

StatuserrorCondition
400invalid_datetimeexpires_at must be a valid ISO 8601 datetime string
400must_be_futureexpires_at must be in the future
400exceeds_max_retentionexpires_at must be within 30 days of the current time

After a file expires:

  • Content and playback routes return 410 with error: "file_expired".
  • GET /api/v1/files/:id still returns metadata (including expires_at) so you can tell why access failed.
  • A background job reaps expired rows within about five minutes of expires_at.

Render-ingested assets: When you pass remote https:// sources in inline render HTML, Editframe downloads and stores them with a one-hour TTL automatically. See Renders.

Not the same as URL signing: URL signing issues short-lived JWTs that control access to endpoints. expires_at controls whether the file record and stored bytes still exist.

Expired file error

json
{
  "error": "file_expired",
  "expires_at": "2026-05-28T12:00:00.000Z",
  "message": "This file expired and is no longer available for download."
}

Deleting a file

To remove a file immediately, call deleteFile with the file's id:

js
import { deleteFile } from "@editframe/api";

const result = await deleteFile(client, file.id);
console.log(result.success); // true

After deletion, the file record and stored bytes are removed immediately. Any composition or render referencing the deleted file-id will fail.

File status values

Using a file in a composition

Reference an uploaded file using its id with the file-id attribute:

html
<ef-configuration api-host="https://editframe.com">
  <ef-timegroup mode="contain" class="w-[1920px] h-[1080px]">
    <ef-video file-id="your-file-id"></ef-video>
  </ef-timegroup>
</ef-configuration>

The file-id is the UUID returned by createFile. It stays the same throughout upload, processing, and playback.

SDK reference

11 of 11