Video APIv1 Home OpenAPI spec

Docs › Build your own player

Build your own player

Use any player you like: a plain <video> tag, hls.js, video.js, Shaka, or a native app. The links come from GET /videos/{id}, which needs your secret API key, so the flow is:

  1. Your server gets the video's links and hands them to the page (never the API key).
  2. The page plays streamUrl (adaptive, Pro and Scale) or playbackUrl (MP4, every plan), with posterPlaybackUrl as the poster.
  3. Before the links expire (playbackExpiresAt), the page asks your server again and swaps the source, keeping the viewer's position. "Refreshing" is just fetching the status again; there's no separate endpoint.

Cache on your server. Your key allows 5 requests per second, so don't call the API once per viewer: keep each video's links and fetch new ones only when less than about a fifth of their lifetime is left. Ask for a long lifetime (?linkTtl=86400) to cache longer.

// Your server (Node.js): GET /api/videos/:id/links → the player's links, cached per video
const cache = new Map();
app.get("/api/videos/:id/links", async (req, res) => {
  const hit = cache.get(req.params.id), now = Date.now() / 1000;
  if (hit && hit.playbackExpiresAt - now > 0.2 * hit.ttl) return res.json(hit);
  const r = await fetch(`${API}/videos/${req.params.id}?linkTtl=86400`, { headers: { Authorization: `Bearer ${API_KEY}` } });
  if (!r.ok) return res.status(r.status).end();
  const s = await r.json();
  const links = { playbackUrl: s.playbackUrl, streamUrl: s.streamUrl, posterUrl: s.posterPlaybackUrl,
                  captionsUrl: s.captions?.vttUrl, playbackExpiresAt: s.playbackExpiresAt, ttl: s.playbackExpiresAt - now };
  cache.set(req.params.id, links);
  res.json(links);
});
<!-- The page: plays HLS where it can, else the MP4, and refreshes the links before they expire -->
<video id="player" controls playsinline crossorigin="anonymous"></video>
<script src="https://cdn.jsdelivr.net/npm/hls.js@1"></script>
<script>
  const video = document.getElementById("player");
  let hls;
  async function load(keepPosition) {
    const links = await (await fetch("/api/videos/VIDEO_ID/links")).json();
    const at = video.currentTime, playing = !video.paused;
    video.poster = links.posterUrl;
    if (links.streamUrl && window.Hls?.isSupported()) {
      hls?.destroy(); hls = new Hls(); hls.loadSource(links.streamUrl); hls.attachMedia(video);
    } else {
      video.src = links.streamUrl && video.canPlayType("application/vnd.apple.mpegurl") ? links.streamUrl : links.playbackUrl;
    }
    if (keepPosition) video.addEventListener("loadedmetadata", () => { video.currentTime = at; if (playing) video.play(); }, { once: true });
    // Refresh a few minutes before expiry (or halfway, for short links)
    const left = links.playbackExpiresAt - Date.now() / 1000;
    setTimeout(() => load(true), Math.max(60, left - 300, left / 2) * 1000);
  }
  load(false);
</script>
  • Captions: add <track kind="subtitles" src="…vttUrl…" srclang="en" default> (the crossorigin attribute on the video lets the browser load it). HLS players pick up the stream's own subtitle track without this.
  • Seeking works for the link's whole lifetime: the MP4 is served with range requests.
  • Expired links answer 403. If a viewer pauses for a long time, call load(true) again on a playback error.
  • Lock links to your sites with allowed domains (below), so nobody can reuse them elsewhere.
  • Easier: use our <video-piper> web component, which does all of this for you, or embed a video with no code at all.