Embed

Play a Ready video on your site with the script-tag embed or your own React player.

Both approaches need one thing from you: a small backend endpoint that mints a playback token using your vdoX API key. Never put your API key in browser code, an API key can create uploads and mint tokens for every video in your workspace; a playback token can only play the one video it was minted for, for a few minutes.

How the flow works

  1. The browser loads your page. The embed script finds [data-vdox-player] and reads its data-video-id.
  2. It POSTs { videoId } to the token endpoint named in data-token-endpoint, which is a route on your backend.
  3. Your backend calls vdoX, server-side, using your API key: it mints a playback token and reads the video's manifest path.
  4. Your backend responds to the browser with { token, manifestUrl }. Your API key never leaves your server.
  5. The browser hands manifestUrl to hls.js (or plays it natively on iPhone) and playback starts.

Script tag

The simplest way to embed a video, no build step. This is the exact snippet shown on each video's page in your dashboard:

htmlHTML
<div data-vdox-player data-video-id="01ARZ3NDEKTSV4RRFFQ69G5FAV"></div>
<script src="https://app.vdox.example.com/embed.js" data-token-endpoint="https://yourapp.example.com/vdox-token"></script>

data-video-id is the video to play. data-token-endpoint on the <script> tag points at your backend's token endpoint (below). The script auto-mounts a player into every matching <div> on the page once it loads.

Your token endpoint

The embed script (and the React example below) both call this endpoint the same way: POST with body { videoId }, nothing else required on the request. It must respond 200 with { token, manifestUrl }:

  • token, its expiry, and an absolute, already-tokenized manifestUrl all come straight back from POST /api/v1/videos/:id/playback-token, called with your API key. Forward that response's fields verbatim, no need to look up the video separately or build the URL yourself.
codeserver.js
// server.js: your backend. Never send your vdoX API key to the browser.
import express from "express";

const app = express();
app.use(express.json());

const VDOX_API_BASE = "https://api.vdox.example.com"; // your vdoX API base
const VDOX_API_KEY = process.env.VDOX_API_KEY; // vdx_..., kept server-side only

app.post("/vdox-token", async (req, res) => {
  const { videoId } = req.body;
  const headers = { Authorization: `Bearer ${VDOX_API_KEY}` };

  // Mint a short-lived playback token for this viewer. The response already
  // includes an absolute, tokenized manifestUrl, ready to hand straight to
  // the browser, no need to separately look up the video or build the URL
  // yourself.
  const tokenRes = await fetch(
    `${VDOX_API_BASE}/api/v1/videos/${videoId}/playback-token`,
    { method: "POST", headers }
  );
  if (!tokenRes.ok) {
    const body = await tokenRes.json();
    return res.status(tokenRes.status).json({ error: body.error.code });
  }
  const { token, manifestUrl } = await tokenRes.json();

  res.json({ token, manifestUrl });
});

app.listen(3001);

See API reference for the full request/response shapes of both calls, including what happens if the video isn't ready yet (409 not_ready).

React

If you're not using the script tag, call your token endpoint yourself and hand the manifest to hls.js (or a native <video> tag on Safari/iOS):

codeVdoxEmbed.tsx
import { useEffect, useRef } from "react";

// Minimal example: fetch a playback token from YOUR backend (never call
// vdoX with your API key from the browser), then hand hls.js the manifest.
export function VdoxEmbed() {
  const containerRef = useRef(null);

  useEffect(() => {
    let hls;
    let cancelled = false;

    (async () => {
      const res = await fetch("YOUR_BACKEND/vdox-token", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ videoId: "01ARZ3NDEKTSV4RRFFQ69G5FAV" }),
      });
      const { token, manifestUrl } = await res.json();
      if (cancelled || !containerRef.current) return;

      const video = document.createElement("video");
      video.controls = true;
      video.style.width = "100%";
      containerRef.current.appendChild(video);

      const { default: Hls } = await import("hls.js");
      if (Hls.isSupported()) {
        hls = new Hls();
        hls.loadSource(manifestUrl);
        hls.attachMedia(video);
      } else {
        video.src = manifestUrl; // Safari/iOS: native HLS
      }
    })();

    return () => {
      cancelled = true;
      if (hls) hls.destroy();
    };
  }, []);

  return <div ref={containerRef} />;
}

Watermark

The hosted embed player (/embed/:videoId) always renders a semi-transparent, moving watermark over the video: a viewer-identifying label on line one, and a short session id, timestamp, and device hash on line two. It repositions on a randomized 6 to 10 second cadence, so a capture can't predict where it'll be, and a MutationObserver re-inserts it if something removes it from the page. This is deterrence, not prevention: browsers have no API to detect or block screen recording, so the watermark exists to trace a captured video back to the viewer, session, and device, not to stop the capture itself. See Mobile security for the same trade-off on native platforms, where actual detection is possible.

This watermark, the capture-deterrence attributes, and the heartbeat/revocation below are all properties of the hosted iframe embed (/embed/:videoId). The script-tag embed (embed.js) attaches a plain <video> element directly to your page, with none of it: no watermark, no capture deterrence, no heartbeat. If you need those protections, load /embed/:videoId directly (Option A in Mobile) with the query params documented next.

Embed URL params

Beyond token, the hosted embed URL (/embed/:videoId?token=...) accepts a few optional query params:

  • &session=<sessionId>: the sessionId returned alongside the token from POST /api/v1/videos/:id/playback-token. Enables a heartbeat (every 30 seconds) so the player stops playback the moment that session is revoked, for example because the workspace exceeded its plan's concurrent-stream limit or an admin revoked it. Without it, the embed still plays, it just can't be remotely stopped mid-stream.
  • &label=<viewer>: the text shown on the watermark's first line, typically the viewer's name or email. Falls back to a generic "vdoX" label when omitted, so the watermark still carries a session/device trace even if you haven't adopted label yet.
  • &pauseOnHide=1: pauses and blurs the video the instant the embedding tab or window is backgrounded (document.visibilitychange going hidden). The viewer has to resume manually, it never auto-resumes. Off by default so existing embeds are unaffected.

All three are additive and independently optional: an older integration that only passes token keeps working exactly as before.