Player API
Control a Playflow player from JavaScript on the page that embeds it. Version 1 provides promise-based playback, state, and page-view appearance methods plus event observers. It does not require or accept a Platform API key.
Origin is required. The iframe URL must contain the exact, URL-encoded origin of its parent page. An origin has a scheme and host, plus a port when needed, but no path, query, fragment, credentials, wildcard, or trailing slash. The origin controls messaging only. It does not alter workspace or video rules for where videos can play.
Set up a player handle
The recommended script embed automatically adds the current page origin to the iframe it creates. Give the placeholder an ID, load https://v.getplayflow.co/embed.js, then call Playflow.player("player-id"). Playflow.apiVersion is 1. For a direct iframe, include origin=https%3A%2F%2Fwww.example.com in its URL and load the same SDK script in the parent page after the iframe element. Existing iframe embeds without origin continue to play, but have no Player API.
<script src="https://v.getplayflow.co/embed.js" data-playflow-workspace="WORKSPACE_ID" defer></script>
<div id="tour-player" data-playflow-video="VIDEO_ID"></div>
<script>
document.addEventListener("DOMContentLoaded", async () => {
const player = Playflow.player("tour-player");
await player.on("ready", state => console.log(state.duration));
});
</script><iframe id="tour-frame"
src="https://v.getplayflow.co/WORKSPACE_ID/e/VIDEO_ID?origin=https%3A%2F%2Fwww.example.com"
allow="autoplay; fullscreen; picture-in-picture; screen-wake-lock"
allowfullscreen></iframe>
<script src="https://v.getplayflow.co/embed.js"></script>
<script>
const player = Playflow.player("tour-frame");
</script>Methods
Every method returns a Promise. Playback, seek, volume, and state commands wait until the actual media can play. on and off register or remove the observer synchronously and return an already resolved promise, so observers remain useful while media is loading or blocked.
| Method | Result |
|---|---|
play() | Promise<void> |
pause() | Promise<void> |
seek(seconds) | Promise<void>. Seconds are explicit and zero-based. |
mute() | Promise<void> |
unmute() | Promise<void> |
setVolume(volume) | Promise<void>. Use a number from 0 to 1. |
getState() | Promise<PlayerState> |
setMode("background" | "standard") | Promise<void>. Change this instance only. Switch to Standard before calling unmute. |
setAppearance(patch) | Promise<Appearance>. Change only the supplied appearance fields for this page view. |
getAppearance() | Promise<Appearance>. Read the published appearance with this page's overrides, including absent-asset no-ops. Timed editor placements are not reported. |
resetAppearance() | Promise<Appearance>. Restore the published workspace/video appearance for this page view. |
on(event, handler) | Promise<void>. Registration happens synchronously. |
off(event, handler) | Promise<void>. Removal happens synchronously. |
State and event detail
getState() and existing playback event handlers receive the same full state snapshot. Event detail may additionally include fullscreen or error. appearancechange reports effective appearance separately. Button and Hotspot notifications report item details separately; they do not add fields to playback state.
| Field | Type |
|---|---|
ready | boolean |
playing | boolean |
mode | "standard" | "background" |
currentTime | number in seconds |
duration | number in seconds |
muted | boolean |
volume | number from 0 to 1 |
chapter | { index, title, startSeconds } or null |
captionsLanguage | language string or null |
blocked | boolean |
fullscreen | optional boolean on relevant events |
error | optional { code, message } |
Events
modechange fires after the mode actually changes and includes the full state with mode. Setting the current mode again does not emit another change.
| Event | When it fires |
|---|---|
modechange | This instance changes between Standard and Background. Includes the full state and its mode. |
ready | Actual media can play. |
play | Playback starts. |
pause | Playback pauses. |
timeupdate | Playback position changes, at most four times per second. |
seeked | A seek completes. |
ended | Playback reaches the end. |
volumechange | Volume or mute state changes. |
chapterchange | The active chapter changes. |
captionschange | The active captions language changes. |
fullscreenchange | Fullscreen state changes. |
error | Playback reports an error. |
blocked | Playback domain policy denies the page. |
appearancechange | Effective page appearance changes; its handler receives Appearance. |
overlayshow | An entitled Button or Hotspot becomes visible at its start time; its handler receives OverlayDetail. |
overlayhide | A visible Button or Hotspot leaves its time window, or becomes unavailable; its handler receives OverlayDetail. |
overlayclick | A viewer activates a Button or Hotspot by click, tap, Enter or Space; its handler receives OverlayDetail. |
To author a Button, Hotspot or timed Play and Replay placement, read Interactivity. The Player API exposes click and visibility notifications, while the documented appearance keys control instance-level appearance; the editor saves content separately.
OverlayDetail has { overlayId, label, type, action }: overlayId is the saved item UUID, label its accessible name, type is hotspot for a Hotspot or text for a Button, and action is open_url, seek, pause, api_event or none. Display-only Buttons can send overlayshow and overlayhide with action: "none" but cannot send overlayclick. A Tell the page click includes name as a fifth field. Register with await player.on("overlayclick", detail => console.log(detail)); remove that same listener with off. Player API callbacks do not themselves record analytics. Only entitled, available Buttons and Hotspots produce these notifications.
Page-view appearance
In workspace Customization, enable Allow pages to restyle the player (on by default). If disabled, appearance methods reject with appearance-disabled. Older embeds without this published setting continue to allow page changes. The workspace and per-video settings remain the baseline.
Only the following keys may be supplied. Unknown keys, URLs, invalid types, colors, or enum values reject the entire patch with invalid-argument; nothing changes. Appearance colors are derived for readable controls rather than painted as raw hex. Changes are live without restarting playback and last only for this page view: they are not saved to workspace, video, browser storage, or signup draft.
| Field | Accepted values | Behavior |
|---|---|---|
accent | Hex color, e.g. #7FD4D0 | Derives a palette subject to contrast floors. |
coverTreatment | default, darken, gradient | Changes the poster treatment. |
barStyle | full, floating | Changes control-bar layout. |
chapters | boolean | Hides chapter controls only; chapterchange continues to work. |
controlBarLogo | boolean | Toggles a published tenant control-bar logo, if present; otherwise a no-op. |
watermarkLogo | boolean | Toggles a published tenant watermark, if present; otherwise a no-op. Never hides required Playflow attribution. |
title | always, never, timed | Changes title overlay mode, not the video title or playback. |
centerPlay | auto, show, hide | Controls the large center Play button while paused and before first play. Auto hides it while a visible Button or Hotspot intersects it. |
centerReplay | auto, show, hide | Controls the large center Replay button after playback ends, independently of centerPlay. Auto hides it while a visible Button or Hotspot intersects it. The control bar and video click gesture still work when hidden. |
centerPlayPosition, centerReplayPosition | {x: 30, y: 50} | Independent positions on all plans. Both coordinates must be finite numbers from 0 to 100, with no extra keys. Percentages refer to the video picture, including letterboxing, crop and fullscreen; the default is {x: 50, y: 50}. Rendering keeps the circle inside the visible picture without changing stored coordinates. Auto checks the resolved, moved rectangle; Hide ignores position. |
centerPlaySize, centerReplaySize | Finite numbers from 8 to 40 | Independent circle diameters as percentages of the shorter video-picture edge, including letterboxing, crop and fullscreen. Unset (null) preserves the existing responsive circle size. Available on all plans as workspace defaults and per-video overrides; a null per-video value inherits the workspace size. Rendering caps the circle to fit inside the visible picture and checks its resolved size for Auto overlap. Hide still hides the circle. |
For example, await player.setAppearance({centerPlayPosition: {x: 30, y: 50}, centerReplayPosition: {x: 70, y: 50}, centerPlaySize: 18, centerReplaySize: 18}) moves and resizes the circles independently for this page view. The existing Allow pages to restyle the player switch applies: when disabled, setAppearance() rejects with appearance-disabled. Changes do not persist. Script embeds accept data-center-play-size="18" and data-center-replay-size="18"; direct iframe and watch URLs accept center_play_size=18 and center_replay_size=18. Unset sizes preserve the responsive size; a circle is capped to fit its player, and the center control is hidden when the player is shorter than 44px on either edge.
Timed Play and Replay placements
On Marina and Fleet, set up to 10 Play and Replay placements per video in the Interactive editor tab; they are separate from the 20 Button and Hotspot limit. Set a start and end time (in seconds, with end after start), choose Play, Replay or Both, then set a picture-relative X/Y position (0–100%), a diameter (8–40% of the picture’s shorter edge) or leave the size unset to retain the size in effect, and choose show or hide. Placements for the same circle cannot overlap; a Both placement occupies both circles’ timelines. They have no click action and create no new Player API events.
The player resolves a visible circle at the current time: before first play at 0, while paused at the paused time, and for Replay after the video ends at its duration. A placement ending exactly at the video duration also applies to ended Replay. Outside a placement, the circle uses its saved video setting, then workspace default, then the player default. A host page’s setAppearance field overrides the timed placement for that field; the complete order is page override, timed placement, per-video setting, workspace default, player default. Auto tests the resolved circle against clickable Buttons and Hotspots. The player clamps the circle to the visible picture and hides center controls if the player’s shortest edge is under 44px. A visible placement change moves and resizes over 150ms, instantly when reduced motion is requested. The video is not reloaded.
getAppearance() and appearancechange describe only the published baseline plus page-view overrides, not a moment-by-moment timed layout. resetAppearance() removes page overrides so the editor placements apply again. You cannot set placements through the Player API: set them in the editor. See Embedding a video for how the same published placements follow the picture inside embeds.
const player = Playflow.player("tour-player");
await player.on("appearancechange", appearance => console.log(appearance));
const appearance = await player.setAppearance({
accent: "#7FD4D0", coverTreatment: "gradient", barStyle: "floating",
chapters: false, controlBarLogo: true, watermarkLogo: false, title: "timed"
});
console.log(appearance, await player.getAppearance());
await player.resetAppearance();The homepage Try it now demo uses the public video zL4NJjVC2R in workspace QAvLDF8REg. To embed that exact video and try temporary appearance changes:
<script src="https://v.getplayflow.co/embed.js" data-playflow-workspace="QAvLDF8REg" defer></script>
<link rel="stylesheet" href="https://getplayflow.co/assets/tokens.css">
<div id="demo-player" data-playflow-video="zL4NJjVC2R" data-sizing="fixed" data-width="1920" data-height="1080" data-autoplay="off"></div>
<script>
document.addEventListener("DOMContentLoaded", async () => {
const accent = getComputedStyle(document.documentElement).getPropertyValue("--lagoon").trim();
await Playflow.player("demo-player").setAppearance({ accent });
});
</script>Copy-paste examples
Enable a button after the video ends
<script src="https://v.getplayflow.co/embed.js" data-playflow-workspace="WORKSPACE_ID" defer></script>
<div id="lesson" data-playflow-video="VIDEO_ID"></div>
<button id="continue" disabled>Continue</button>
<script>
document.addEventListener("DOMContentLoaded", () => {
const player = Playflow.player("lesson");
player.on("ended", () => {
document.getElementById("continue").disabled = false;
});
});
</script>Record a play in your CRM bridge
The browser sends only public event data to your own same-origin endpoint. Keep CRM credentials on your server and never put secrets in this page.
<script src="https://v.getplayflow.co/embed.js" data-playflow-workspace="WORKSPACE_ID" defer></script>
<div id="demo" data-playflow-video="VIDEO_ID"></div>
<script>
document.addEventListener("DOMContentLoaded", () => {
const player = Playflow.player("demo");
player.on("play", state => {
fetch("/crm/video-play", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ videoId: "VIDEO_ID", currentTime: state.currentTime })
});
});
});
</script>Jump to a chapter
<script src="https://v.getplayflow.co/embed.js" data-playflow-workspace="WORKSPACE_ID" defer></script>
<div id="webinar" data-playflow-video="VIDEO_ID"></div>
<button id="pricing-chapter">Jump to pricing</button>
<script>
document.addEventListener("DOMContentLoaded", () => {
const player = Playflow.player("webinar");
document.getElementById("pricing-chapter").addEventListener("click", async event => {
const startSeconds = 92;
await player.seek(startSeconds);
await player.play();
});
});
</script>Observe blocked playback with a direct iframe
<iframe id="policy-player"
src="https://v.getplayflow.co/WORKSPACE_ID/e/VIDEO_ID?origin=https%3A%2F%2Fwww.example.com"
allow="autoplay; fullscreen; picture-in-picture; screen-wake-lock" allowfullscreen></iframe>
<script src="https://v.getplayflow.co/embed.js"></script>
<p id="player-status"></p>
<script>
const player = Playflow.player("policy-player");
player.on("blocked", state => {
document.getElementById("player-status").textContent =
state.error?.message || "This video is not allowed on this site.";
});
player.play().catch(error => {
if (error.code === "blocked") console.info(error.message);
});
</script>Errors and blocked playback
Use try/catch or .catch() for every playback or state method. If domain policy blocks the page, queued and future calls to play, pause, seek, mute, unmute, setVolume, and getState reject with an Error whose code and message are blocked. Observer registration with on and off stays usable, so register a blocked handler before attempting playback.
Starting quality is a published playback preference, not a Player API method. Set data-start-quality="auto|360|480|720|1080|1440|2160" on an individual script-embed div or start_quality on a direct iframe or watch URL. Starts here, then adjusts to the viewer's connection like Auto. The minimal embed remains valid; the player inherits the video setting. Only available source renditions are used, without upscaling. In-play Quality selection is separate and can fix a quality until Auto is selected. See Starting quality and preload.
Calls can also reject for invalid arguments (invalid-argument), disabled workspace page styling (appearance-disabled), an unavailable API, or a removed or reloaded iframe. Browser autoplay policy can reject play(); prompt the viewer to interact rather than retrying silently.
Background mode and Watch with sound
Use Playflow.player("hero-video") or Playflow.player(document.getElementById("hero-video")) to address an individual instance. Two embeds of the same video have independent handles, playback and modes. Background still accepts play(), pause() and getState(). unmute() rejects with background-muted until you switch to Standard.
document.getElementById("watch-with-sound").onclick = async () => {
const player = Playflow.player("hero-video");
await player.setMode("standard");
await player.play();
await player.unmute();
const state = await player.getState();
if (!state.playing || state.muted) {
// Leave Standard controls visible so the viewer can press Play.
}
};
await Playflow.player("hero-video").on("modechange", state => console.log(state.mode));The host-click sequence was verified with actual audio playback in headed Chromium, without bypassing its autoplay policy. Browser policies can still require an interaction inside the iframe. The portable fallback is to call setMode("standard") and let the viewer press the player's Play button. Switching back with setMode("background") hides controls and input and forces muted looping again. See Background video embeds for click-through and watermark rules.