Your server already knows the class ID, title, provider and private source URL.
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.
Unpirator finds/updates the asset and returns playbackRef.
The private source URL never enters HTML, React props or browser Network.
Your playback endpoint checks the logged-in user before Unpirator creates a session.
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.
Loads the class/video, knows the private source, knows who the student is and decides whether that student can watch.
Registers/resolves the source, returns playbackRef, creates short-lived playback sessions and protects media delivery.
Receives playbackRef, creates stable device metadata, calls your playback endpoint and handles the protected playback lifecycle.
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.
pb_7dk…91q sourceUrl: never rendereddb.lesson.find("physics-12")privateVideoUrl = https://media…/master.m3u8POST /api/unpirator/playbackviewer → course access → allow / denycontentId + provider + private sourcepb_7dk…91qCache-Control: no-storeNothing in this demo calls a live customer database. It visualizes the exact production responsibility split.
Do these three things — in this order.
video.id→ externalContentIdvideo.privateUrl→ sourceUrlplaybackRef→ pass this to the playerimport { 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} />;
}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);
},
});resolveViewer andauthorizePlayback are required. resolveViewer must return the authenticated viewer email; protected playback does not accept a browser-chosen guest identity."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)}
/>
);
}No browser-trusted identity. Here is where each field comes from.
playbackRefFrom Step 1Unpirator returns it after your server resolves/upserts the video.
deviceIdRequired · generated automaticallyThe 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 serverYour 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 serverForward the incoming viewer request context so Security Center shows the viewer device/network context instead of your Node/PHP/Python server request.
siteIdFrom server environmentThe verified Unpirator site ID copied from your dashboard.
POST /api/unpirator/playback
Content-Type: application/json
{
"playbackRef": "pb_7dk...91q",
"deviceId": "device_...",
"client": {
"browser": "Chrome",
"os": "Windows"
}
}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.A playback-session response contains short-lived authorization material. no-storetells browsers, proxies and CDNs not to cache and reuse that response.
Install the layer made for your stack.
@unpirator/reactReact player
UI component for React and Next.js.
@unpirator/integration-nextjsNext.js server bridge
Server helpers for asset upsert and protected session creation.
@unpirator/web-componentUniversal player
Custom element for PHP, Laravel, Django, Flask and HTML templates.
@unpirator/sdk-jsBrowser SDK
Device identity and session/bootstrap helpers for custom integrations.
@unpirator/playerPlayback runtime
Low-level grants, refresh, heartbeat, HLS and watermark runtime.
Prepare the workspace once.
Create a site
Use the exact production hostname where the player will run.
Verify the hostname
Complete one of the offered verification methods.
Enable your provider
Configure the provider/connection used by your video source.
Create a server API key
Keep it in server-only secret storage. Never expose it with NEXT_PUBLIC_ or VITE_.
UNPIRATOR_API_URL=https://theunpirator.vercel.app/control-api
UNPIRATOR_API_KEY=up_live_replace_me
UNPIRATOR_SITE_ID=00000000-0000-0000-0000-000000000000Where it comes from, where it goes, and what it is not.
The four rules that should never be optional.
- Keep
UNPIRATOR_API_KEYon your server only. - Keep the original private source URL on your server only.
- Authenticate and authorize the viewer before each protected playback session.
- Return playback-session responses with
Cache-Control: no-store.
Find the failing boundary quickly.
400 · VALIDATION_ERRORA required field is missing or has the wrong type.
401 · UNAUTHORIZEDThe server API key is missing, expired or incorrect.
403 · FORBIDDENSite, domain, policy or viewer access was rejected.
404 · NOT_FOUNDThe site or asset does not exist in this workspace.
409 · CONNECTION_DISABLEDThe linked provider connection is disabled.
429 · RATE_LIMITToo many requests. Respect Retry-After.
DOMAIN_MISMATCHThe playback hostname does not match the verified site.
SOURCE_INVALIDThe source URL/provider configuration could not be used.