Prevent unauthorized viewers from downloading your content or embedding your player on sites that you do not own.
JWP allows you to secure your media and players by requiring requests to use signed URLs. These URLs are valid for only a short period of time, preventing unauthorized downloading of your media or embedding of your player on unapproved sites.
Signed URLs are generated using three inputs: the resource path, an expiration time, and a property’s secret key.
When a signed request reaches JWP:
- From one of your properties, a request is sent to JWP that includes an expiration and a signature.
- JWP validates the signature.
- If the signature is valid, the request succeeds. Otherwise, the request is denied.
JWP's resources support the following signing methods.
| Method | Description |
|---|---|
| JSON web token (JWT) | URL secured with a JSON Web Token (JWT) that encodes claims (like path, expiration, and optional parameters) and signed with your property’s secret key using HMAC-SHA256 |
| Non-JWT | URL secured with a hash signature generated by applying MD5 to the path, expiration, and your property’s secret key |
Available resource routes
Most resource routes require JWT signing. A few routes require non-JWT signing. If a route supports both methods, we strongly recommend using JWT signing.
The table below lists each supported resource route and its signing method.
| Signing method | Routes |
|---|---|
| Advertising schedule | |
| JWT | /v2/advertising/schedules/{ad_schedule_id}.{ad_schedule_extension} |
| Media | |
| JWT | /v2/sites/{site_id}/media/{media_id}/playback.json |
| JWT | /v2/media/{media_id} |
| Players | |
| Non-JWT | /libraries/{player_id}.js |
| Non-JWT | /thumbs/{media_id}-{thumb_width}.jpg |
| Non-JWT | /players/{content_id}-{player_id}.{embed_type} |
| Playlist | |
| JWT | /v2/playlists/{playlist_id} |
| JWT | /v2/playlists/{playlist_id}/drm/{policy_id} |
| Poster image | |
| JWT | /v2/media/{media_id}/poster.jpg |
| Non-JWT | /previews/{content_id}-{player_id} |
| Streaming manifests | |
| Non-JWT | /manifests/{media_id}.{manifest_extension} |
| Text tracks | |
| Non-JWT | /tracks/{track_id}.{track_extension} |
| Video files | |
| Non-JWT | /videos/{media_id}-{template_id}.{media_extension} |
Prerequisite
| Item | Description |
|---|---|
| Signing secret | Token used to sign the URL to prevent unauthorized content downloads and embedding
Follow these steps to obtain the secret:
|
Create a signed URL
JWT
When generating a signed URL, note the following:
- The following code samples are provided for guidance and may not work in your environment. If you use any of these samples, be sure to test the functionality in a development environment before deploying it into a production environment.
- For security reasons, JWT-signed URLs MUST always be generated on the server side.
Follow these steps to sign a URL:
-
Copy the following snippet to your code.
The following code snippets use the
/v2/sites/{site_id}/media/{media_id}/playback.jsonresource route. Adjust the samples accordingly for the resource route used.import jwt from 'jsonwebtoken'; // Configuration (edit these values) const signing_secret = 'SIGNING_SECRET'; const site_id = 'SITE_ID'; const media_Id = 'MEDIA_ID'; const path = `/v2/sites/${site_id}/media/${media_id}/playback.json`; function jwtSignedUrl(path) { const host = 'https://cdn.jwplayer.com'; const now = Math.floor(Date.now() / 1000); const payload = { resource: path, // Put any accepted query params here, not in the URL, for example: // related_media_id: 'RltV8MtT', exp: Math.ceil((now + 3600) / 300) * 300, // valid for 1h }; const token = jwt.sign(payload, signing_secret, { algorithm: 'HS256', noTimestamp: true, // omit "iat" to maximize cacheability }); return `${host}${path}?token=${token}`; } // Generate the signed URL const url = jwtSignedUrl(path); console.log(url);import math import time import os import requests from jose import jwt # pip install python-jose requests signing_secret = os.environ.get("SIGNING_SECRET") # Define your variables site_id = "SITE_ID" media_id = "MEDIA_ID" path = f"/v2/sites/{site_id}/media/{media_id}/playback.json" def jwt_signed_url(path): """ Generate a signed URL with JWT. """ host = "https://cdn.jwplayer.com" now = int(time.time()) payload = { "resource": path, # Add any supported query parameters here # "related_media_id": "RltV8MtT", "exp": math.ceil((now + 3600) / 300) * 300, } token = jwt.encode(payload, signing_secret, algorithm="HS256") return f"{host}{path}?token={token}" # Generate the signed URL url = jwt_signed_url(path) -
Define the
signing_secret,site_id,media_id, andpathvalues. -
(Optional) Add supported route parameters to the
payload.All URL parameters that you want to include must be included in the payload as separate parameters.
Do not append them to the
path. Any URL parameter added to a JWT-signed request will be ignored if it is not within the payload. -
Optional) Adjust the
expcalculation or enter a UNIX timestamp in seconds.Note the following when setting
exp:- Typical range: 1 minute to several hours. Shorter durations make signed content more secure but can cause playback issues if the URL expires during playback (especially for long videos or playlists).
- On high-traffic sites, cache signed URLs to prevent performance issues related to generating signed URLs. For example, cache URLs in intervals of five minutes. Signed requests do not need to be unique.
- Expirations under one minute may cause playback errors due to clock drift or network delays.
Now, you can enable URL signing functionality for your property.
Non-JWT
When generating a signed URL, note the following:
- The following code samples are provided for guidance and may not work in your environment. If you use any of these samples, be sure to test the functionality in a development environment before deploying it into a production environment.
- For security reasons, JWT-signed URLs should always be generated on the server side.
Follow these steps to sign a URL:
-
Copy the following snippet to your code.
The following snippets use the
/players/{content_id}-{player_id}.{embed_type}resource route. Adjust the samples accordingly for the resource route used.import MD5 from 'crypto-js/md5'; // Configuration (edit these values) const signing_secret = 'SIGNING_SECRET'; const content_id = 'CONTENT_ID'; const player_id = 'PLAYER_ID'; const embed_type = 'js'; // e.g., 'js' | 'html' (as supported) /** * Build a non-JWT signed URL for: * /players/{content_id}-{player_id}.{embed_type} * Link valid ~1 hour, normalized to 5 minutes for caching. */ function signedPlayerEmbedUrl(content_id, player_id, embed_type) { const host = 'https://cdn.jwplayer.com'; const path = `players/${content_id}-${player_id}.${embed_type}`; const now = Math.floor(Date.now() / 1000); const expires = Math.ceil((now + 3600) / 300) * 300; // 1h, 5m rounding const base = `${path}:${expires}:${signing_secret}`; const signature = MD5(base).toString(); return `${host}/${path}?exp=${expires}&sig=${signature}`; } // Generate the signed URL const url = signedPlayerEmbedUrl(content_id, player_id, embed_type); console.log(url);import hashlib import math import os import time # Configuration (edit these values) signing_secret = os.environ.get("SIGNING_SECRET") content_id = "CONTENT_ID" # Replace with your media ID player_id = "PLAYER_ID" # Replace with your player ID embed_type = "js" # e.g., "js" | "html" (as supported) def signed_player_embed_url(content_id, player_id, embed_type): """ Build a non-JWT signed URL for: /players/{content_id}-{player_id}.{embed_type} Link valid ~1 hour, normalized to 5 minutes for caching. """ host = "https://cdn.jwplayer.com" path = f"players/{content_id}-{player_id}.{embed_type}" now = int(time.time()) expires = math.ceil((now + 3600) / 300) * 300 # 1h, 5m rounding base = f"{path}:{expires}:{signing_secret}" signature = hashlib.md5(base.encode("utf-8")).hexdigest() return f"{host}/{path}?exp={expires}&sig={signature}" # Generate the signed URL url = signed_player_embed_url(content_id, player_id, embed_type) print(url) -
Define the
signing_secret,content_id,player_id, andembed_typevalues. -
Optional) Adjust the
expirescalculation or enter a UNIX timestamp in seconds.Note the following when setting
expires:- Typical range: 1 minute to several hours. Shorter durations make signed content more secure but can cause playback issues if the URL expires during playback (especially for long videos or playlists).
- On high-traffic sites, cache signed URLs to prevent performance issues related to generating signed URLs. For example, cache URLs in intervals of five minutes. Signed requests do not need to be unique.
- Expirations under one minute may cause playback errors due to clock drift or network delays.
- For
/players/{content_id}-{player_id}.htmland/previews/{content_id}-{player_id}.html, JWP only enforces protection up to three hours in the future. However, for DRM content, the protection window will be the same as the DRM policy's license duration.
Now, you can enable URL signing functionality for your property.
Enable URL signing functionality
After you have created signed URLs for all of your content, you must enable URL signing functionality for your properties.

URL signing section
Follow these steps to enable the URL signing functionality:
- On the Properties page, click the name of a property. The settings page for the property appears.
- On the Content & ad enhancements, click Content protection. The Content protection tab appears.
- Under URL signing, enable one or both protection options:
- Secure video URLs
- Secure player embeds & HLS playlists
The following table explains the secure signing behavior when one or both settings are enabled.
| Content | Secure video URLs: ON
Secure player embeds & HLS playlists: OFF |
Secure video URLs: OFF
Secure player embeds & HLS playlists: ON |
Secure video URLs: ON
Secure player embeds & HLS playlists: ON |
|---|---|---|---|
| Playlists (JSON/MRSS) | Must be signed | N/A | Must be signed and have signed link in response |
| HLS playlists (.m3u8) | No signing required | Must be signed | Must be signed |
| MPEG-DASH manifests (.mpd) | No signing required | Must be signed | Must be signed |
| Progressive videos (.mp4, JWP-hosted) | Must be signed | N/A | Must be signed and have signed link in response |
| Cloud-hosted player libraries | N/A | Must be signed, but no signed link in response required | Must be signed and have signed link in response |
| Single-line embed players | N/A | Must be signed, but no signed link in response required | Must be signed and have signed link in response |
| Images, text tracks | No signing required | No signing required | No signing required |
Error handling
| Error code | Error message | Possible conditions |
|---|---|---|
200 | Success | Content is requested via a signed URL when URL signing is enabled for all content. |
403 | Access forbidden | Content is requested via an unsigned URL when URL signing is enabled for all publisher content. Content is requested via an incorrectly signed URL when URL signing is enabled for all content. |
FAQ
Does URL token signing work the same for JWP hosted and externally hosted (registered) media?
No.
For JW Platform hosted media, both the request to the Delivery API and all media URLs will be signed.
For externally hosted (registered) media, only the request to the Delivery API and the returned media URL will be signed. The signed media URL from the Delivery API response will redirect (HTTP status code 302) to the externally hosted media URL. The JW Platform is not currently able to sign the externally hosted content URL.
