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_at | Meaning |
|---|---|
null (omitted on create) | Permanent — kept until you call deleteFile |
| ISO 8601 datetime | File is removed shortly after this time |
After a file expires:
- Content and playback routes return 410 with
error: "file_expired". GET /api/v1/files/:idstill returns metadata (includingexpires_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
| Status | Description |
|---|---|
created | File registered, awaiting upload |
uploading | Upload in progress |
processing | Upload complete, video being transcoded |
ready | File available for use in compositions |
failed | Processing 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.