For advanced use cases, use tokens to authenticate viewers accessing your content. This allows for more granular access control than country, domain, or IP rules alone.
When using token protection, your website or application must generate a token. A shared secret derived from a Blue Billywig “Secret” is necessary for token calculation. The shared secret remains known only to the client and Blue Billywig platform, ensuring only authorized websites can calculate valid tokens.
There are two token types:
- TOTP (RPC Token): a time-based one-time password that rotates automatically; used for simple integrations where you only need time-based access control.
- JWT: a signed JSON payload with custom claims and expiration; used for advanced integrations requiring per-user or per-content access control.
An interactive JWT demo is available here.
How it works
- Your backend retrieves the shared secret (stored securely server-side);
- Your backend generates a TOTP or JWT token;
- The token is passed to the player via query parameter or HTTP header;
- The OVP validates the token against the shared secret;
- If valid, video playback is allowed; if invalid, playback is blocked.
Before implementing token-based content protection, ensure you have:
- A Blue Billywig OVP publication;
- Access to the OVP management console (specifically: Publication settings > Secrets and Publication settings > Content protection);
- A Content Protection Policy with a Token rule configured (see 1.0 | Create a Token Secret and 2.0 | Create a Content Protection Policy with a Token Rule);
- A backend server capable of generating tokens (the shared secret must never be exposed client-side).
1.0 | Create a Token Secret
To create a token:
- In the OVP sidebar, navigate to Publication settings > Secrets;
- On the Secrets overview page, click the Create new content protection token button.
![]()
- Fill in the form:
- Label (required): a descriptive name for your secret (e.g., “Production JWT Token”).
- Description (optional): additional context about the secret’s purpose.
- Expiration settings: set the token validity duration using the Days, Hours, and Minutes fields. The expiration must be at least 1 minute. Once the token expires, it becomes invalid and a new token needs to be generated.
- Click Save & Activate to create and immediately activate the secret, or click Save to create it in an inactive state.
- After saving, the secret is generated. You can find it in the Secret field on the detail page (click Show secret to reveal the value).
![]()
- The Details panel on the right shows:
- ID: the numeric secret identifier (you will need this for TOTP tokens).
- Secret type: confirms “Content protection”.
- Label, Created, and Modified metadata.
- Copy the secret value and store it securely in your backend (e.g., in environment variables or a secrets manager).
2.0 | Create a Content Protection Policy with a Token Rule
- In the OVP sidebar, navigate to Publication settings > Content protection;
- On the Content protection policies overview page, click Create new content protection policy;
- Under Policy name, fill in a Title and optional Description;
- Under Select policy type, configure the rules:
- Click the Add rule dropdown and select Token;
- Choose an operator (e.g., is, is not, is empty, is not empty);
- If using is or is not, select the content protection token you created in 1.0 | Create a Token Secret.
- You can combine multiple rules:
- Click Add rule to add an additional condition within the same rule set (AND logic);
- Click Add rule set to add an alternative set of conditions (OR logic).
- Click Save to create the policy.
Blocked Content Display
When a token is invalid or missing, the policy determines how blocked content appears to viewers:
- Media clips: either show the player with playback disabled, or hide the content entirely (a background color is displayed in its place).
- Channels: either show the channel with playback disabled, or hide it completely (a blank screen appears).
These display options are configured when creating the policy (see Blocked Content Display in the Content Protection Policies article).
Apply the Policy
After creating the policy, assign it to your content:
- Media clips: navigate to Media library > Media clips, open a clip, go to the Content Protection tab, and select your policy.
- Channels: navigate to Media library > Channels, open a channel, go to the General tab, scroll to Content Protection, and select your policy.
3.0 | TOTP Token
Authenticating a request using a TOTP token requires two elements:
- The ID of the Secret;
- The generated token itself.
TOTP (Time-based One-Time Password) tokens use the RFC 6238 standard. The token value rotates automatically based on the configured time step (expiration window).
Token Format
The generated token must use a time-step duration matching the secret’s expiration time in seconds. Append the generated token to the ID with a hyphen:
{SECRET_ID}-{GENERATED_TOTP_CODE}For example: 42-839205. Include this string in the rpctoken header or query parameter.
Generating a TOTP Token
Node.js (using speakeasy)
const speakeasy = require("speakeasy");
const SECRET_ID = "42";
const SHARED_SECRET = "your-shared-secret";
const EXPIRATION = 86400; // 24 hours in seconds (must match OVP setting)
function generateRpcToken() {
const totp = speakeasy.totp({ secret: SHARED_SECRET, digits: 6, step: EXPIRATION });
return `${SECRET_ID}-${totp}`;
}
const token = generateRpcToken(); // e.g. "42-839205"Python (using pyotp)
import pyotp
import base64
SECRET_ID = "42"
SHARED_SECRET = "your-shared-secret"
EXPIRATION = 86400 # 24 hours in seconds
def generate_rpc_token():
# pyotp expects a base32-encoded secret
secret_b32 = base64.b32encode(SHARED_SECRET.encode()).decode()
totp = pyotp.TOTP(secret_b32, digits=6, interval=EXPIRATION)
code = totp.now()
return f"{SECRET_ID}-{code}"
token = generate_rpc_token() # e.g. "42-839205"PHP (using OTPHP)
<?php
use OTPHP\TOTP;
$secretId = '42';
$sharedSecret = 'your-shared-secret';
$expiration = 86400; // 24 hours
function generateRpcToken(string $secretId, string $secret, int $expiration): string {
$totp = TOTP::createFromSecret($secret);
$totp->setPeriod($expiration);
$totp->setDigits(6);
$code = $totp->now();
return "{$secretId}-{$code}";
}
$token = generateRpcToken($secretId, $sharedSecret, $expiration); // e.g. "42-839205"Passing the TOTP Token
The token can be sent as a query parameter or HTTP header:
- Query parameter (appended to the embed URL):
https://{publication}.bbvms.com/p/{playout}/c/{clipId}.js?rpctoken=42-839205 - HTTP header:
rpctoken: 42-839205
4.0 | JWT Token
When authenticating a request using a JWT token, sign the payload with the shared secret. JWT tokens offer more flexibility than TOTP: you can include custom claims such as expiration time, subject identifiers, and entity-level access restrictions.
JWT Structure
The JWT must be signed using HMAC SHA-256 (HS256) with the shared secret. The payload should include at minimum:
{
"sub": "user-identifier",
"iat": 1700000000,
"exp": 1700000900
}| Claim | Description | Required |
|---|---|---|
sub | Subject identifier (e.g., user ID or session ID) | Recommended |
iat | Issued-at timestamp (Unix epoch seconds) | Recommended |
exp | Expiration timestamp (Unix epoch seconds) | Yes |
entity_access | Entity-level access restrictions (see 6.0 | Entity-Level Access Control) | Optional |
Generating a JWT Token
Node.js (using jsonwebtoken)
const jwt = require("jsonwebtoken");
const SHARED_SECRET = "your-shared-secret";
function generateJwtToken(userId) {
const now = Math.floor(Date.now() / 1000);
const payload = { sub: userId, iat: now, exp: now + 900 }; // 15 minutes
return jwt.sign(payload, SHARED_SECRET, { algorithm: "HS256" });
}
const token = generateJwtToken("user-123");Python (using PyJWT)
import jwt
import time
SHARED_SECRET = "your-shared-secret"
def generate_jwt_token(user_id: str) -> str:
now = int(time.time())
payload = {
"sub": user_id,
"iat": now,
"exp": now + 900, # 15 minutes
}
return jwt.encode(payload, SHARED_SECRET, algorithm="HS256")
token = generate_jwt_token("user-123")PHP (using firebase/php-jwt)
<?php
use Firebase\JWT\JWT;
$sharedSecret = 'your-shared-secret';
function generateJwtToken(string $userId): string {
global $sharedSecret;
$now = time();
$payload = [
'sub' => $userId,
'iat' => $now,
'exp' => $now + 900, // 15 minutes
];
return JWT::encode($payload, $sharedSecret, 'HS256');
}
$token = generateJwtToken('user-123');Passing the JWT Token
The calculated JWT can be provided via the Authorization header as a Bearer token, or through the jwt header or query parameter. These are checked in the following order:
- Query parameter (appended to the embed URL):
https://{publication}.bbvms.com/p/{playout}/c/{clipId}.js?jwt=eyJhbGciOi... - HTTP header jwt:
jwt: eyJhbGciOi... - Authorization Bearer header:
Authorization: Bearer eyJhbGciOi...
5.0 | Embedding with Tokens
In a typical integration, your backend generates the token and appends it as a query parameter to the embed URL. Blue Billywig supports several embed methods (read more about embedding). Below are the most common approaches with token authentication.
JavaScript Embed
Append the token as a query parameter to the .js embed URL:
<!-- TOTP -->
<script
type="text/javascript"
src="https://{publication}.bbvms.com/p/{playout}/c/{clipId}.js?rpctoken=42-839205"
async="true"
></script>
<!-- JWT -->
<script
type="text/javascript"
src="https://{publication}.bbvms.com/p/{playout}/c/{clipId}.js?jwt=eyJhbGciOi..."
async="true"
></script>iFrame Embed
Append the token to the .html embed URL in the src attribute:
<iframe
onload="
this.src +=
'#!referrer=' + encodeURIComponent(location.href) + '&realReferrer=' + encodeURIComponent(document.referrer)"
src="https://{publication}.bbvms.com/p/{playout}/c/{clipId}.html?inheritDimensions=true&jwt=eyJhbGciOi..."
width="720"
height="405"
frameborder="0"
allowfullscreen
allow="autoplay; fullscreen"
></iframe>Server-Side Token Injection
For maximum security, have your backend generate the token and inject it into the embed URL so that the token never appears in static HTML source code:
// Express.js example: renders a page with the token embedded in the player URL
const jwt = require("jsonwebtoken");
app.get("/video/:clipId", (req, res) => {
const token = generateJwtToken(req.user.id);
const embedUrl = `https://${PUBLICATION}.bbvms.com/p/${PLAYOUT}/c/${req.params.clipId}.js?jwt=${token}`;
res.send(`
<html>
<body>
<script type="text/javascript" src="${embedUrl}" async="true"></script>
</body>
</html>
`);
});6.0 | Entity-Level Access Control (JWT)
JWT tokens support fine-grained access control through the entity_access claim. This allows you to restrict a token to specific media clips, clip lists, or projects.
Claim Structure
The entity_access claim is an object where keys are entity types and values are arrays of allowed entity IDs:
{
"sub": "user-123",
"iat": 1700000000,
"exp": 1700000900,
"entity_access": {
"mediaclip": ["12345", "67890"],
"mediacliplist": ["101"],
"project": ["42"]
}
}| Entity Type | Description |
|---|---|
mediaclip | Individual video clips |
mediacliplist | Clip lists / playlists |
project | Projects |
Behavior
- If the entity_access claim is omitted or empty, the token grants access to all content protected by the policy (no entity-level restriction).
- If entity_access is present, the OVP checks whether the requested content matches one of the allowed entities. Access is granted if any entity type/ID pair matches.
- An empty instances array (
"mediaclip": []) matches all instances of that entity type.
Example: Restricting to a Single Clip
const jwt = require("jsonwebtoken");
function generateClipToken(userId, clipId) {
const now = Math.floor(Date.now() / 1000);
const payload = {
sub: userId,
iat: now,
exp: now + 900,
entity_access: { mediaclip: [String(clipId)] }
};
return jwt.sign(payload, SHARED_SECRET, { algorithm: "HS256" });
}
// This token only grants access to clip 12345
const token = generateClipToken("user-123", 12345);