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.

Recommended script embed
<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>
Direct iframe
<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.

MethodResult
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.

FieldType
readyboolean
playingboolean
mode"standard" | "background"
currentTimenumber in seconds
durationnumber in seconds
mutedboolean
volumenumber from 0 to 1
chapter{ index, title, startSeconds } or null
captionsLanguagelanguage string or null
blockedboolean
fullscreenoptional boolean on relevant events
erroroptional { 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.

EventWhen it fires
modechangeThis instance changes between Standard and Background. Includes the full state and its mode.
readyActual media can play.
playPlayback starts.
pausePlayback pauses.
timeupdatePlayback position changes, at most four times per second.
seekedA seek completes.
endedPlayback reaches the end.
volumechangeVolume or mute state changes.
chapterchangeThe active chapter changes.
captionschangeThe active captions language changes.
fullscreenchangeFullscreen state changes.
errorPlayback reports an error.
blockedPlayback domain policy denies the page.
appearancechangeEffective page appearance changes; its handler receives Appearance.
overlayshowAn entitled Button or Hotspot becomes visible at its start time; its handler receives OverlayDetail.
overlayhideA visible Button or Hotspot leaves its time window, or becomes unavailable; its handler receives OverlayDetail.
overlayclickA 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.

FieldAccepted valuesBehavior
accentHex color, e.g. #7FD4D0Derives a palette subject to contrast floors.
coverTreatmentdefault, darken, gradientChanges the poster treatment.
barStylefull, floatingChanges control-bar layout.
chaptersbooleanHides chapter controls only; chapterchange continues to work.
controlBarLogobooleanToggles a published tenant control-bar logo, if present; otherwise a no-op.
watermarkLogobooleanToggles a published tenant watermark, if present; otherwise a no-op. Never hides required Playflow attribution.
titlealways, never, timedChanges title overlay mode, not the video title or playback.
centerPlayauto, show, hideControls the large center Play button while paused and before first play. Auto hides it while a visible Button or Hotspot intersects it.
centerReplayauto, show, hideControls 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, centerReplaySizeFinite numbers from 8 to 40Independent 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.

Live appearance, without saving
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:

Homepage Try it now video
<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

HTML and JavaScript
<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.

HTML and JavaScript
<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

HTML and JavaScript
<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

HTML and JavaScript
<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.

Host-page button
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.