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:

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:

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.

Wait for processing

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

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.

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.

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_atMeaning
null (omitted on create)Permanent — kept until you call deleteFile
ISO 8601 datetimeFile is removed shortly after this 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

{
  "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:

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

StatusDescription
createdFile registered, awaiting upload
uploadingUpload in progress
processingUpload complete, video being transcoded
readyFile available for use in compositions
failedProcessing failed

Using a file in a composition

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

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