Videos
Upload videos to a video library from the terminal, check how they are doing, and get the code to put them on a page.
Video libraries are currently in Beta. There are limits on how much you can store, and features and pricing may change.
A video library holds your videos. When you upload a file, we make several versions of it at different sizes, so it plays well on a fast connection and on a slow one. When that is done you get a link and a snippet you can paste into a page.
Simplifyd has reverse-billing agreements with mobile networks, so the data cost of watching is charged to you instead of to the viewer's data balance. People can watch even with no data left.
The edge video commands are for uploading and managing those files. Use them when you have more than a handful of videos, or when you want uploading to be part of a script.
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 needs all three. The easiest way is to set them once:
edge linkOr pass them on each command with -w, -p and -e.
Name the library. Every command takes --library (short: -l), which is the name or id of your video library. This is never guessed for you: a project can hold more than one library, and putting a customer's video in the wrong one is a mistake you would not notice until someone shares the wrong link.
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]The file goes straight to storage, so a large file is no harder than a small one. It is sent in parts, a few at a time, and each part is retried on its own if the connection hiccups.
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 is only drawn when you are watching a terminal. In a script, or with --json, it is left out so it does not clutter your logs or break your output.
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
A dropped connection does not throw away your upload. Everything that already arrived is kept, and the command tells you how to carry on:
3 of 214 parts are stored. Carry on with:
edge video upload ./talk.mp4 --library training --resume 0192f3c1-8a44-7c31-9d10-6b2e4f9a1c77Run that and only the missing parts are sent. On a slow or unreliable connection this is the difference between finishing a large file and never finishing it.
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.
- An unfinished upload is still yours. 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
}In Go there is no reason to spawn anything. The CLI is a thin wrapper over the Go SDK, and calling it directly gives you the same upload — parts, retries, resume — with real errors instead of parsed text:
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)Which to reach for
| 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 200 MiB per request. |
| The Go SDK | You are writing Go. Everything the CLI does, as function calls. |
Whichever you use, the video id you get back is the same id, and the video is the same video.
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 | It arrived and is waiting its turn. |
processing | We are preparing it, with a percentage as it goes. |
ready | It plays. There is a link and an embed snippet. |
failed | Something about the file could not be handled. The reason is shown. |
get
Show one video in full: status, the versions we made, captions, and the links.
edge video get <video-id> -l trainingIf a video failed, this is where you see why, in plain words — for example that the file uses a format we cannot read. That tells you what to fix, which a red mark on a page does not.
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 frame version is the default because it is the one that works everywhere without you changing anything.
A video only has an embed once it is ready.
Embed options
The snippet is a starting point. Both forms take options, so one video can behave differently on the pages it appears on.
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 allow list as printed. clipboard-write is what lets the player's copy items reach the clipboard; without it they fall back to an older method that some browsers have already dropped, and a viewer is told the page would not allow the copy.
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" | Autoplay, muted. Spelled muted rather than true because that is the only autoplay there is. |
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. |
Take data-base and the script URL from the snippet rather than typing them. Every other address — the playlist, the poster, the player, and hls.js — is built from data-base, and the player's path carries a version that changes when the player does.
What a viewer can do
The player replaces the browser's own right-click menu with one that fits a video. Right-clicking again inside that menu gives you the browser's, so nothing is taken away.
| 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 | The same address, at the moment on screen — this is how a video gets quoted. |
| Copy embed code | The frame snippet, the same one edge video embed prints. |
| Playback stats | Data used, the version being played and its bitrate, the buffer, and dropped frames. It is there so the claim that watching is free can be checked rather than taken on trust. On Safari and on iPhones the browser plays the video itself and keeps the byte count to itself, so the panel says so instead of guessing. |
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. |
Alongside the usual figures — views, watch time, how far people get — you also see how much of the watching cost the viewer nothing, broken down by mobile network. That is the number that tells you whether your audience is really watching for free.
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 trainingThe time can be seconds (42), or mm:ss, or hh:mm:ss. The video keeps playing the whole time and the link does not change — the picture is simply replaced a few seconds later.
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 --yesThis cannot be undone. --yes skips the confirmation, which is what you want in a script.
Example script: upload a folder of videos
This is the common case — you have a folder of files and want them all in a library, without sitting and watching it.
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
doneRun it and leave it. Uploading is the slow part; preparing the videos happens afterwards, so the script does not wait for it.
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 |
this workspace holds N hours of video, which is the current limit | The workspace is at its limit | Delete a video, or contact support to have the limit raised |
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 |