Simplifyd Cloud

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 training

Before 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 link

Or 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 training

upload

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

FlagDescription
--titleTitle for the video. Defaults to the file name without its extension.
--waitWait until the video is ready to play before the command returns.
--concurrencyHow many parts to send at once. Default 4.
--retriesHow many times to try a part before giving up. Default 5.
--resumeFinish 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 training

Example 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-6b2e4f9a1c77

The 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-6b2e4f9a1c77

Run 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 list with the status uploading. Either finish it with --resume, or remove it with edge 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 subprocessThe 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 APINothing 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 SDKYou 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 200

Flags

FlagDescription
--limitMost videos to return. Default 50.
--offsetHow 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 3

What the statuses mean

StatusMeaning
uploadingThe file has not finished arriving. Resume it, or remove it.
queuedIt arrived and is waiting its turn.
processingWe are preparing it, with a percentage as it goes.
readyIt plays. There is a link and an embed snippet.
failedSomething 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 training

If 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 training

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

Use --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>
ParameterWhat it does
tStart somewhere other than the beginning. Takes seconds (90), a clock reading (1:30), or 1m30s.
autoplay=1Start 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>
AttributeWhat it does
data-startWhere 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-titleAn 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.

ItemWhat it does
Play, PauseThe same as clicking the picture.
LoopStart again at the end. Stays ticked until it is turned off.
Playback speedHalf speed up to double.
Picture in picturePop the video out of the page, where the browser allows it.
Copy video linkThe video's own address.
Copy link at current timeThe same address, at the moment on screen — this is how a video gets quoted.
Copy embed codeThe frame snippet, the same one edge video embed prints.
Playback statsData 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:

KeyWhat it does
Space or KPlay or pause
Back or forward five seconds
Volume
MMute
FFull screen
Menu or Shift+F10Open 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-31

Flags

FlagDescription
--fromStart date, YYYY-MM-DD. Defaults to four weeks ago.
--toEnd 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 training

The 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 training

The 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 --wait

Use 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 --yes

This 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
done

Run 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

MessageCauseFix
a video library is required: pass --library <name>No library namedAdd --library <name>
"..." is not a video container we can readThe file is not a type we acceptUse .mp4, .mov, .mkv, .webm or .m4v
that file is ...; the limit for a single upload is 8.0 GiBThe file is too largeSplit it, or ask us to raise the limit
this workspace holds N hours of video, which is the current limitThe workspace is at its limitDelete 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 fileResume with the same file, or upload this one on its own
this video is processing; an embed exists once it is readyThe video is not finished yetWait, then run edge video embed again
not authenticatedNo token foundRun edge auth login, or set CLOUD_TOKEN