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
- The browser loads your page. The embed script finds
[data-vdox-player]and reads itsdata-video-id. - It
POSTs{ videoId }to the token endpoint named indata-token-endpoint, which is a route on your backend. - Your backend calls vdoX, server-side, using your API key: it mints a playback token and reads the video's manifest path.
- Your backend responds to the browser with
{ token, manifestUrl }. Your API key never leaves your server. - The browser hands
manifestUrlto 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:
<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-tokenizedmanifestUrlall come straight back fromPOST /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.
// 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):
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>: thesessionIdreturned alongside the token fromPOST /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 adoptedlabelyet.&pauseOnHide=1: pauses and blurs the video the instant the embedding tab or window is backgrounded (document.visibilitychangegoing 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.