Videos
Upload videos, check processing status, and retrieve playback links and embed code.
Video libraries are currently in Beta. There are limits on how much you can store, and features and pricing may change.
A video library stores videos and prepares them for playback at multiple resolutions. Once processing is complete, each video has playback links and embed code.
On supported mobile networks, playback data charges are billed to your workspace instead of the viewer. Viewers on these networks can watch without a data balance.
Use edge video to upload and manage videos from your terminal or scripts.
edge video upload talk.mp4 --library trainingBefore you start
Create the library first. You create a video library in the web console, on your project canvas. The CLI works with a library that already exists; it does not make one.
Set your workspace, project and environment. Every command below requires all three. Set them for the current directory:
edge linkOr pass them on each command with -w, -p and -e.
Specify the library. Every command requires --library (short: -l), set to the name or ID of the target video library.
edge video list --library training
edge video list -l trainingupload
Upload a video file and start preparing it for playback.
edge video upload <file> --library <library> [flags]Uploads use multiple parts. Failed parts are retried individually; use --concurrency and --retries to control this behaviour.
Flags
| Flag | Description |
|---|---|
--title | Title for the video. Defaults to the file name without its extension. |
--wait | Wait until the video is ready to play before the command returns. |
--concurrency | How many parts to send at once. Default 4. |
--retries | How many times to try a part before giving up. Default 5. |
--resume | Finish an upload that was cut short. See If an upload is interrupted. |
Accepted files: .mp4, .mov, .mkv, .webm and .m4v.
Examples
# Upload with the file name as the title
edge video upload talk.mp4 -l training
# Give it a proper title
edge video upload talk.mp4 -l training --title "Opening the account"
# Wait until it is ready, then print the embed code
edge video upload talk.mp4 -l training --wait
edge video embed <video-id> -l trainingExample output
[============================] 100.0% 1.4 GiB / 1.4 GiB
✓ Uploaded Opening the account
Field Value
ID 0192f3c1-8a44-7c31-9d10-6b2e4f9a1c77
Status queued
Source 1.4 GiB
Encoding has been queued. Follow it with: edge video get 0192f3c1-8a44-7c31-9d10-6b2e4f9a1c77The progress bar appears only in an interactive terminal. It is omitted in scripts and with --json.
JSON output
edge video upload talk.mp4 -l training --json{
"slug": "0192f3c1-8a44-7c31-9d10-6b2e4f9a1c77",
"title": "Opening the account",
"status": "queued",
"source_bytes": 1503238553,
"has_source": true,
"created_at": "2026-09-04T09:12:44Z"
}The links and the embed snippet appear once the status is ready. Add --wait if you want the command to return only at that point.
If an upload is interrupted
Completed parts are retained after an interrupted upload. The command prints a resume command:
3 of 214 parts are stored. Carry on with:
edge video upload ./talk.mp4 --library training --resume 0192f3c1-8a44-7c31-9d10-6b2e4f9a1c77Run the resume command to upload only the missing parts.
Two things to know:
- Use the same file. The size is checked against what the upload started with, so you cannot accidentally finish one upload with a different file.
- Unfinished uploads remain listed. It shows in
edge video listwith the statusuploading. Either finish it with--resume, or remove it withedge video rm.
Calling it from your application
edge video upload --json prints the video as JSON on stdout and nothing else, which makes it usable as a subprocess wherever the file is already on disk — a queue worker, a build step, a backend handling an upload from your own users.
Set CLOUD_TOKEN and pass the context on the command line, so the behaviour does not depend on which directory the process happens to run in:
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
const run = promisify(execFile);
export async function uploadVideo(path: string, title: string) {
const { stdout } = await run(
'edge',
[
'video', 'upload', path,
'--library', 'training',
'--title', title,
'--json',
'-w', process.env.SIMPLIFYD_WORKSPACE!,
'-p', process.env.SIMPLIFYD_PROJECT!,
'-e', process.env.SIMPLIFYD_ENV!,
],
{ env: { ...process.env, CLOUD_TOKEN: process.env.SIMPLIFYD_API_TOKEN } },
);
return JSON.parse(stdout);
}execFile, not exec: it passes the arguments as a list rather than through a shell, so a title with a quote or a semicolon in it is a title rather than a second command.
<?php
function uploadVideo(string $path, string $title): array
{
$command = [
'edge', 'video', 'upload', $path,
'--library', 'training',
'--title', $title,
'--json',
'-w', getenv('SIMPLIFYD_WORKSPACE'),
'-p', getenv('SIMPLIFYD_PROJECT'),
'-e', getenv('SIMPLIFYD_ENV'),
];
$descriptors = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
$process = proc_open(
$command,
$descriptors,
$pipes,
null,
['CLOUD_TOKEN' => getenv('SIMPLIFYD_API_TOKEN')] + $_ENV,
);
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
array_map('fclose', $pipes);
if (proc_close($process) !== 0) {
throw new RuntimeException(trim($stderr));
}
return json_decode($stdout, true);
}Pass the command as an array. proc_open then runs the binary directly instead of handing the string to a shell, and a filename with a space in it stays one argument.
// The fields worth naming from the CLI's JSON. It prints the same video
// document the API returns, so add fields as you need them.
type Video struct {
Slug string `json:"slug"`
Title string `json:"title"`
Status string `json:"status"`
SourceBytes int64 `json:"source_bytes"`
}
func UploadVideo(ctx context.Context, path, title string) (*Video, error) {
cmd := exec.CommandContext(ctx, "edge",
"video", "upload", path,
"--library", "training",
"--title", title,
"--json",
"-w", os.Getenv("SIMPLIFYD_WORKSPACE"),
"-p", os.Getenv("SIMPLIFYD_PROJECT"),
"-e", os.Getenv("SIMPLIFYD_ENV"),
)
cmd.Env = append(os.Environ(), "CLOUD_TOKEN="+os.Getenv("SIMPLIFYD_API_TOKEN"))
// Only stdout carries the document. Progress and errors go to stderr, and
// mixing them into the same buffer would leave you parsing a progress bar.
var stdout, stderr bytes.Buffer
cmd.Stdout, cmd.Stderr = &stdout, &stderr
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("edge video upload: %s", strings.TrimSpace(stderr.String()))
}
var video Video
if err := json.Unmarshal(stdout.Bytes(), &video); err != nil {
return nil, err
}
return &video, nil
}Go applications can use the Go SDK directly. It supports multipart uploads, retries, and resuming interrupted uploads:
client := cloud.NewClient(cloud.WithToken(os.Getenv("SIMPLIFYD_API_TOKEN")))
videos := client.Workspace(ws).Project(proj).Env(env).Services().Video(library)
video, err := videos.Upload(ctx, path, title, nil)Choosing an interface
| The CLI as a subprocess | The file is on disk, edge is installed, and you want parts, retries and --resume without writing them. Good in CI and in workers you control. |
| The HTTP API | Nothing to install, and the file need never touch a disk — a request handler can pass an upload straight through. Capped at 300 MiB per request. |
| The Go SDK | You are writing Go. Everything the CLI does, as function calls. |
Video IDs can be used across the CLI, HTTP API, and Go SDK.
list
List the videos in a library, newest first.
edge video list -l training
edge video list -l training --limit 200Flags
| Flag | Description |
|---|---|
--limit | Most videos to return. Default 50. |
--offset | How many to skip, for paging. Default 0. |
Example output
ID TITLE STATUS LENGTH STORED
0192f3c1-8a44-7c31-9d10-6b2e4f9a1c77 Opening the account ready 12:04 286.4 MiB
0192f3bd-1c02-7a55-b3e9-77c1de40aa31 Card replacement processing 62% — 0 B
0192f3b8-9d71-70e2-84cc-2f0b6a51f4d2 Lost PIN failed: this… — 0 B
3 of 3What the statuses mean
| Status | Meaning |
|---|---|
uploading | The file has not finished arriving. Resume it, or remove it. |
queued | Upload complete; waiting for processing. |
processing | Processing in progress, with a completion percentage. |
ready | Playback links and embed code are available. |
failed | Processing failed. The error message describes the cause. |
get
Show a video's status, available resolutions, captions, and playback links.
edge video get <video-id> -l trainingFor failed videos, the response includes an error message, such as an unsupported file format.
embed
Print the snippet to put the video on a page.
edge video embed <video-id> -l trainingThe snippet is printed on its own, with nothing around it, so you can pipe it straight into a template:
edge video embed <video-id> -l training >> page.htmlUse --inline for the version that places the player inside your own page markup, rather than in a frame of its own. The default iframe embed isolates the player from the surrounding page styles.
A video only has an embed once it is ready.
Embed options
Both iframe and inline embeds support per-embed playback options.
On the frame embed
Options go on the URL in src:
<iframe src="https://<library>.video.simplifyd.app/e/<video-id>?t=90&autoplay=1"
width="640" height="360" loading="lazy"
allow="fullscreen; picture-in-picture; clipboard-write"
style="border:0;aspect-ratio:16/9;width:100%;height:auto"></iframe>| Parameter | What it does |
|---|---|
t | Start somewhere other than the beginning. Takes seconds (90), a clock reading (1:30), or 1m30s. |
autoplay=1 | Start playing as soon as the page loads. The player mutes itself to do it, because muted is the only autoplay a browser allows; the viewer can unmute. |
t is what the player's own Copy link at current time produces, so a link someone sends you from a video is one of these.
Keep the generated allow attribute, including clipboard-write, so the player's copy actions can access the clipboard.
On the inline embed
Options are attributes on the div:
<div data-sfd-video="<video-id>"
data-base="https://<library>.video.simplifyd.app"
data-start="90"
data-autoplay="muted"></div>
<script src="https://<library>.video.simplifyd.app/player/<version>/player.js" defer></script>| Attribute | What it does |
|---|---|
data-start | Where to start, in the same forms t takes. |
data-autoplay="muted" | Start playback muted. Use the value muted. |
data-title | An accessible name for the player, for a page where the surrounding text does not already give one. |
data-datafree="false" | Leave out the lines that tell the viewer watching costs them no data. |
data-beacon="" | Turn the playback beacon off for this embed. Nothing is then measured for these views, and they do not appear in edge video stats. |
Use the data-base value and script URL from the generated snippet. Do not construct or modify these URLs manually.
What a viewer can do
Right-click the player to open its video controls. Right-click again inside that menu to open the browser's context menu.
| Item | What it does |
|---|---|
| Play, Pause | The same as clicking the picture. |
| Loop | Start again at the end. Stays ticked until it is turned off. |
| Playback speed | Half speed up to double. |
| Picture in picture | Pop the video out of the page, where the browser allows it. |
| Copy video link | The video's own address. |
| Copy link at current time | A video link that starts at the current playback time. |
| Copy embed code | The frame snippet, the same one edge video embed prints. |
| Playback stats | Data usage, current resolution and bitrate, buffer, and dropped frames. Byte counts are unavailable in browsers that do not report them, including native playback on Safari and iPhone. |
The keyboard works throughout, so the player is usable without a pointer:
| Key | What it does |
|---|---|
Space or K | Play or pause |
← → | Back or forward five seconds |
↑ ↓ | Volume |
M | Mute |
F | Full screen |
Menu or Shift+F10 | Open the right-click menu; arrows move through it, Esc closes it |
stats
Show how a video is being watched. Leave off the id to see the whole library.
edge video stats <video-id> -l training
edge video stats -l training --from 2026-08-01 --to 2026-08-31Flags
| Flag | Description |
|---|---|
--from | Start date, YYYY-MM-DD. Defaults to four weeks ago. |
--to | End date, YYYY-MM-DD. Defaults to today. |
Statistics include views, watch time, completion, and data-free playback broken down by mobile network.
captions
Attach a WebVTT caption file, or remove one.
edge video captions add <video-id> captions.en.vtt -l training
edge video captions rm <video-id> <track-id> -l trainingThe language is taken from the file name when it is written like captions.en.vtt. Set it yourself with --language, and set the name viewers see with --label.
poster
Choose the still frame shown before someone presses play.
edge video poster <video-id> 1:07 -l trainingSpecify the time in seconds (42), mm:ss, or hh:mm:ss. The poster updates after a few seconds. Playback and video links are unaffected.
This needs the library to be keeping original files.
reprocess
Prepare a video again from the original file you uploaded.
edge video reprocess <video-id> -l training --waitUse this after changing a library setting that you want applied to a video already uploaded. It does the work again, so it is charged again.
rm
Delete a video and everything made from it.
edge video rm <video-id> -l training
edge video rm <video-id> -l training --yesDeletion is permanent. Pass --yes to skip confirmation in scripts.
Example script: upload a folder of videos
Use this script to upload a folder of videos to a library.
The script uploads each file, skips anything already there, and picks up where it left off if the connection drops.
#!/usr/bin/env bash
# Upload every video in a folder to a library.
# Needs: edge, jq
# Run `edge auth login` and `edge link` first, so the workspace,
# project and environment are already set.
set -uo pipefail
LIBRARY="training"
FOLDER="./videos"
ATTEMPTS=5 # tries per file before moving on
# What is already in the library, so a rerun does not upload twice.
existing=$(edge video list --library "$LIBRARY" --limit 1000 --json \
| jq -r '(.videos // [])[].title')
shopt -s nullglob
for file in "$FOLDER"/*.mp4 "$FOLDER"/*.mov; do
title=$(basename "${file%.*}")
if grep -Fxq "$title" <<< "$existing"; then
echo "-- $title is already there, skipping"
continue
fi
echo "==> $title"
resume=""
for attempt in $(seq 1 "$ATTEMPTS"); do
if [ -n "$resume" ]; then
out=$(edge video upload "$file" --library "$LIBRARY" --resume "$resume" 2>&1)
else
out=$(edge video upload "$file" --library "$LIBRARY" 2>&1)
fi
code=$?
echo "$out"
if [ $code -eq 0 ]; then
echo " done"
break
fi
# When an upload is cut short the CLI prints the command to carry on.
# Take the id out of it and send only what is missing.
resume=$(grep -o -- '--resume [^ ]*' <<< "$out" | tail -1 | cut -d' ' -f2)
if [ -z "$resume" ]; then
echo " could not be uploaded, moving on"
break
fi
echo " interrupted, trying again ($attempt of $ATTEMPTS)"
sleep 5
done
doneThe script waits for uploads to complete. Video processing continues after the script exits.
Checking on them afterwards
# Anything still being worked on
edge video list -l training --limit 1000
# Anything that did not work, with the reason
edge video list -l training --limit 1000 --json \
| jq -r '(.videos // [])[] | select(.status == "failed") | "\(.title): \(.status_message)"'Errors
| Message | Cause | Fix |
|---|---|---|
a video library is required: pass --library <name> | No library named | Add --library <name> |
"..." is not a video container we can read | The file is not a type we accept | Use .mp4, .mov, .mkv, .webm or .m4v |
that file is ...; the limit for a single upload is 8.0 GiB | The file is too large | Split it, or ask us to raise the limit |
video ... was registered for a N-byte file and ... is M bytes | --resume was given a different file | Resume with the same file, or upload this one on its own |
this video is processing; an embed exists once it is ready | The video is not finished yet | Wait, then run edge video embed again |
not authenticated | No token found | Run edge auth login, or set CLOUD_TOKEN |