DEVELOPER DOCUMENTATION · TESTED 0.2.x

Your videos stay yours.
Playback stays protected.

Keep source URLs out of the browser. Your server sends them privately to Unpirator, and Unpirator handles protected playback.

I'm building with
install · Next.js
$ npm install @unpirator/react@0.2.0 @unpirator/integration-nextjs@0.2.0
Package installed — continue with the server steps below
01 · START HERE

One class page. Two server calls. No source URL in the browser.

You do not need to move your existing video library into an Unpirator admin panel. Your own database remains the source of truth.

1Load your existing video on your server

Your server already knows the class ID, title, provider and private source URL.

2Send that server-side video data to Unpirator

Unpirator finds/updates the asset and returns playbackRef.

3Render only playbackRef into our player

The private source URL never enters HTML, React props or browser Network.

4The player requests playback automatically

Your playback endpoint checks the logged-in user before Unpirator creates a session.

Important: “lesson”, “video”, “course” and your database functions belong to YOUR application.

Unpirator does not magically know your lesson.id, source URL or course permissions. The examples below show where your existing app data maps into the integration.

YOUR APPYour existing application

Loads the class/video, knows the private source, knows who the student is and decides whether that student can watch.

UNPIRATORUnpirator

Registers/resolves the source, returns playbackRef, creates short-lived playback sessions and protects media delivery.

AUTOMATICThe player package

Receives playbackRef, creates stable device metadata, calls your playback endpoint and handles the protected playback lifecycle.

02 · WATCH THE FULL JOURNEY

See exactly where every value comes from.

This animation follows one student from the moment they enter an existing class page until the first protected frame starts. Watch where the private source stops and where playbackRef begins.

INTERACTIVE ARCHITECTURE

Watch one student open one existing class.

This is the complete path — from your database record to the first protected frame.

academy.example.com/classes/physics-12
PHYSICS · CLASS 12Wave motion & oscillation
Open this class
playbackRef: pb_7dk…91q sourceUrl: never rendered
CUSTOMER SERVER
Load existing videodb.lesson.find("physics-12")
Private fields available hereprivateVideoUrl = https://media…/master.m3u8
Automatic player requestPOST /api/unpirator/playback
Authenticate + authorizeviewer → course access → allow / deny
UNPIRATOR
SERVER → SERVERResolve / upsert assetcontentId + provider + private source
RESPONSEReturn playbackRefpb_7dk…91q
PROTECTED SESSIONShort-lived grant + gatewayCache-Control: no-store
PRIVATE SOURCE
playbackRef
session request
READY
Press Start animation

Nothing in this demo calls a live customer database. It visualizes the exact production responsibility split.

Customer code Unpirator Private — browser never receives this
03 · INTEGRATE YOUR STACK

Do these three things — in this order.

STEP 1 · SERVER-SIDEWherever your app already loads a class/video, send that private video data to Unpirator and receive playbackRef.

You choose the file/controller/loader. This runs on your server because the private source URL and API key must never enter the browser. Unpirator receives and stores the source only on its server side as protected asset metadata.

EXAMPLE NAMEvideo.id→ externalContentId
EXAMPLE NAMEvideo.privateUrl→ sourceUrl
UNPIRATOR RETURNSplaybackRef→ pass this to the player
Your class page / server loader
import { createUnpiratorServerClient } from "@unpirator/integration-nextjs";

const unpirator = createUnpiratorServerClient({
  apiUrl: process.env.UNPIRATOR_API_URL,
  apiKey: process.env.UNPIRATOR_API_KEY,
});

export default async function ClassPage({ params }) {
  // YOUR APP: load the video the same way you already do today.
  const lesson = await db.lesson.findUnique({
    where: { id: params.id },
  });

  // YOUR APP decides which fields map to Unpirator.
  // lesson.id and lesson.privateVideoUrl are examples from YOUR database.
  const { playbackRef } = await unpirator.upsertPlaybackAsset({
    siteId: process.env.UNPIRATOR_SITE_ID,
    externalContentId: String(lesson.id),
    title: lesson.title,
    provider: "hls",
    sourceUrl: lesson.privateVideoUrl,
    allowedHosts: ["media.example.com"],
    providerConfig: {},
  });

  // Only playbackRef crosses into the rendered player.
  return <LessonVideo playbackRef={playbackRef} />;
}
STEP 2 · SERVER-SIDE SECURITY BOUNDARYCreate one playback endpoint that authenticates and authorizes the student.

The player calls this endpoint automatically. Any functions such as getLoggedInUser or hasCourseAccess are intentionally marked as YOUR APP because only your application knows its auth and purchase rules.

app/api/unpirator/playback/route.js
import { createUnpiratorPlaybackHandler } from "@unpirator/integration-nextjs";
import { auth } from "@/auth";

export const runtime = "nodejs";

export const POST = createUnpiratorPlaybackHandler({
  apiUrl: process.env.UNPIRATOR_API_URL,
  apiKey: process.env.UNPIRATOR_API_KEY,
  siteId: process.env.UNPIRATOR_SITE_ID,

  // YOUR APP: replace this with your real authentication.
  resolveViewer: async () => {
    const session = await auth();

    if (!session?.user?.id || !session?.user?.email) {
      const error = new Error("Sign in required");
      error.status = 401;
      throw error;
    }

    return {
      id: session.user.id, // stays inside YOUR authorization callback
      email: session.user.email, // trusted identity forwarded to Unpirator
    };
  },

  // YOUR APP: replace this with your real course/subscription check.
  authorizePlayback: async ({ body, viewer }) => {
    const lesson = await db.lesson.findFirst({
      where: { unpiratorPlaybackRef: body.playbackRef },
    });

    if (!lesson) return false;

    return hasCourseAccess(viewer.id, lesson.courseId);
  },
});
The Next.js helper is deny-by-default: resolveViewer andauthorizePlayback are required. resolveViewer must return the authenticated viewer email; protected playback does not accept a browser-chosen guest identity.
STEP 3 · BROWSERRender the player with playbackRef — never with the private source URL.

The playbackRef came from Step 1. After mount, the player automatically calls the endpoint from Step 2.

components/lesson-video.jsx
"use client";

import { UnpiratorPlayer } from "@unpirator/react";

export function LessonVideo({ playbackRef }) {
  return (
    <UnpiratorPlayer
      playbackRef={playbackRef}
      endpoint="/api/unpirator/playback"
      title="Lesson video"
      onError={(error) => console.error(error.code, error)}
    />
  );
}
04 · WHAT THE PLAYER SENDS

No browser-trusted identity. Here is where each field comes from.

playbackRefFrom Step 1

Unpirator returns it after your server resolves/upserts the video.

deviceIdRequired · generated automatically

The official SDK generates a random stable UUID. It is not a hardware fingerprint. It links the authenticated email to the device/session history and enables device controls.

emailRequired · resolved by YOUR server

Your backend reads the authenticated viewer email from its own trusted session or verified access token. The browser never supplies a trusted viewer email.

viewerIp / viewerUserAgentDerived by YOUR server

Forward the incoming viewer request context so Security Center shows the viewer device/network context instead of your Node/PHP/Python server request.

siteIdFrom server environment

The verified Unpirator site ID copied from your dashboard.

Automatic browser request
POST /api/unpirator/playback
Content-Type: application/json

{
  "playbackRef": "pb_7dk...91q",
  "deviceId": "device_...",
  "client": {
    "browser": "Chrome",
    "os": "Windows"
  }
}
Your server adds the trusted email, siteId, viewer request context and server-only API key before calling Unpirator. The player supplies the required random stable deviceId. Any email included in browser JSON must be ignored.
Why Cache-Control: no-store?

A playback-session response contains short-lived authorization material. no-storetells browsers, proxies and CDNs not to cache and reuse that response.

05 · PACKAGES

Install the layer made for your stack.

01
@unpirator/react

React player

UI component for React and Next.js.

02
@unpirator/integration-nextjs

Next.js server bridge

Server helpers for asset upsert and protected session creation.

03
@unpirator/web-component

Universal player

Custom element for PHP, Laravel, Django, Flask and HTML templates.

04
@unpirator/sdk-js

Browser SDK

Device identity and session/bootstrap helpers for custom integrations.

05
@unpirator/player

Playback runtime

Low-level grants, refresh, heartbeat, HLS and watermark runtime.

06 · DASHBOARD SETUP

Prepare the workspace once.

STEP 1

Create a site

Use the exact production hostname where the player will run.

STEP 2

Verify the hostname

Complete one of the offered verification methods.

STEP 3

Enable your provider

Configure the provider/connection used by your video source.

STEP 4

Create a server API key

Keep it in server-only secret storage. Never expose it with NEXT_PUBLIC_ or VITE_.

.env.local
UNPIRATOR_API_URL=https://theunpirator.vercel.app/control-api
UNPIRATOR_API_KEY=up_live_replace_me
UNPIRATOR_SITE_ID=00000000-0000-0000-0000-000000000000
07 · ABOUT PLAYBACKREF

Where it comes from, where it goes, and what it is not.

Your DB videoYour serverUnpirator asset resolveplaybackRefPlayer
It is returned by Unpirator after server-side asset resolution.
It is safe to pass to the authorized playback page.
It does not contain the private source URL or provider Authorization header.
It is not the permission check. Your server still authorizes the logged-in viewer for every session.
08 · SECURITY RULES

The four rules that should never be optional.

  1. Keep UNPIRATOR_API_KEY on your server only.
  2. Keep the original private source URL on your server only.
  3. Authenticate and authorize the viewer before each protected playback session.
  4. Return playback-session responses with Cache-Control: no-store.
09 · ERRORS & DIAGNOSIS

Find the failing boundary quickly.

400 · VALIDATION_ERROR

A required field is missing or has the wrong type.

401 · UNAUTHORIZED

The server API key is missing, expired or incorrect.

403 · FORBIDDEN

Site, domain, policy or viewer access was rejected.

404 · NOT_FOUND

The site or asset does not exist in this workspace.

409 · CONNECTION_DISABLED

The linked provider connection is disabled.

429 · RATE_LIMIT

Too many requests. Respect Retry-After.

DOMAIN_MISMATCH

The playback hostname does not match the verified site.

SOURCE_INVALID

The source URL/provider configuration could not be used.

READY TO INTEGRATE?

Connect your first existing class.

Open integration setup