Private Videos
Restricting Playback
This guide is helpful for when you have a video you want to secure. This video could be paid content or sensitive.
By signing, even if someone has the Edge Key, they cannot play the video back.
What This Does Not Accomplish
This does not make a video bulletproof unpirate-able.
DRMs protect videos, not JWTs.
It does, however, make the process of stealing and disseminating a video much more challenging.
High-Level Workflow
- Lock the video. Create or update an Edge Key with
isPrivate: true. - Sign. Your backend code will generate a JSON Web Token (JWT) using your private signing key.
- Play. Take the JWT you generate and add it to the URL of the video.
Important: Tokens Are Bound to Edge Keys
Caution
Each token is strictly bound to a specific Edge Key. A token signed for edge_A will only grant access to resources requested via edge_A. It will NOT work for edge_B, even if both keys point to the same underlying video.
This design provides critical security benefits:
- Granular Revocation: Revoking
edge_Aimmediately invalidates all tokens for that key, without affectingedge_B. - Isolated Access Control: Different Edge Keys can have different access policies (e.g., one for web, one for mobile app).
- No Cross-Key Leakage: A leaked token for one key cannot be used to access content via another key.
Practical Implication: When generating signed URLs, always use the same Edge Key in:
- The URL path (e.g.,
/v1/{edgeKey}/master.m3u8) - The token claims (
subandaudmust match the Edge Key)
If you store a thumbnailUrl that contains a default Edge Key, but generate a token for a different Edge Key, the request will fail. See Thumbnails for how to construct proper signed thumbnail URLs.
Step 1: Generating Your Signing Key
Under “API Keys” in your app.antcdn.net dashboard, select the option to create a new signing key.
You can choose to create a signing key which works for all your environments in your organization, or scoped to development, staging or production.
Save both the signing key and the key ID.
Step 2: Creating a JWT For Playback
When you create your JWT, there are a few arguments you should be aware of which you’ll be using in your code.
| Field | Type | Description |
|---|---|---|
sub | string | Subject. Pass through one Edge Key or ”*” to allow playback of all signed videos. |
iss | string | Issuer. Must match the issuer you specified when creating the signing key. Your backend is the token issuer, so use your domain or app name (e.g., "my-company.com"). |
aud | string | Audience. Must match the sub (Video ID or Edge Key). |
kid | string | Key ID. Include the ID of the key you generated in step 1. |
expiresIn | number | Expiration. How long the video can be watched, in seconds. |
Tip
Custom Issuer: When you create a signing key, you specify an issuer identifier. This identifier ties your tokens to your organization—use your domain name or application name. The edge verifies that the token’s iss claim matches the expected issuer for that key.
import jwt from "jsonwebtoken";
const privateKey = Buffer.from( process.env.ANTCDN_PRIVATE_KEY!, "base64" // If you saved your private key as base64, you need to do this. If it's just a string, omit this.);
// The issuer you specified when creating your signing keyconst ISSUER = process.env.ANTCDN_ISSUER || "my-company.com";
function signPlaybackToken( // Edge Key you want to allow // Pass "*" to allow all private Edge Keys edgeKey: string, // The ID of the key you generated in step 1. keyID: string, expiresInSeconds: number): string { return jwt.sign( { sub: edgeKey, iss: ISSUER, // Must match the issuer you set when creating the key aud: edgeKey }, privateKey, { algorithm: "RS256", expiresIn: expiresInSeconds, keyid: keyID } );}import base64import osfrom datetime import datetime, timedelta, timezone
import jwt # PyJWT
PRIVATE_KEY = base64.b64decode(os.environ["ANTCDN_PRIVATE_KEY"])KEY_ID = os.environ["ANTCDN_KEY_ID"]ISSUER = os.environ.get("ANTCDN_ISSUER", "my-company.com") # Your issuer
def sign_playback_token(edge_key: str, expires_in_seconds: int) -> str: """ edge_key: Private Edge Key the viewer may access. Pass "*" to allow all private Edge Keys expires_in_seconds: Lifetime of the token in seconds. """ payload = { "sub": edge_key, "iss": ISSUER, # Must match the issuer you set when creating the key "aud": edge_key, "exp": datetime.now(timezone.utc) + timedelta(seconds=expires_in_seconds) }
headers = {"kid": KEY_ID} # Signing key ID from the Antcdn dashboard
return jwt.encode(payload, PRIVATE_KEY, algorithm="RS256", headers=headers)
token = sign_playback_token("edge_PRIVATE_123", 15 * 60)print(token)package main
import ( "encoding/base64" "log" "os" "time"
"github.com/golang-jwt/jwt/v5")
// Helper function to sign a private Edge Key (or "*" for all videos)func signPlaybackToken( // Edge Key of the video you want to allow // Pass "*" to allow all private Edge Keys edgeKey string, // The ID of the key you generated in step 1. keyID string, expiresIn time.Duration ) (string, error) { // edgeKey: private Edge Key the viewer may access // keyID: signing key ID from the Antcdn dashboard // expiresIn: lifetime of the token
keyBytes, err := base64.StdEncoding.DecodeString(os.Getenv("ANTCDN_PRIVATE_KEY")) if err != nil { return "", err }
privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(keyBytes) if err != nil { return "", err }
claims := jwt.MapClaims{ "sub": edgeKey, "iss": os.Getenv("ANTCDN_ISSUER"), // Must match the issuer you set when creating the key "aud": edgeKey, "exp": jwt.NewNumericDate(time.Now().Add(expiresIn)), }
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) token.Header["kid"] = keyID
return token.SignedString(privateKey)}
func main() { token, err := signPlaybackToken( "edge_PRIVATE_123", os.Getenv("ANTCDN_KEY_ID"), 15*time.Minute, ) if err != nil { log.Fatal(err) }
log.Println("Signed JWT:", token)}Step 3: Playing Back Your Video
From step 2 you have a string. This is your JWT.
When you embed a URL, this key must be present to play your video.
Whereas before you might have had a video look like this:
<!-- Iframe example for public video --><iframe src="https://worker.antcdn.net/v1/{edgeKey}/master.m3u8" frameborder="0" allowfullscreen></iframe>Your protected video needs to look like this:
<!------------------------- Add the key as a URL parameter here ⬇️ --><iframe src="https://worker.antcdn.net/v1/{edgeKey}/master.m3u8?jwt={jwt}" frameborder="0" allowfullscreen></iframe>Tip
For private videos, playlists, segments, thumbnails, and captions are all protected. Keep the jwt=... query param on the URLs you embed/request.
Using Other Players
Under “Playing Videos” on the left, we have a few examples of video players you can use.
With each player, you pass through a URL like:
https://player.antcdn.net/v1/{edgeKey}/master.m3u8
Similar to the iframe, add the jwt URL param like so:
https://player.antcdn.net/v1/{edgeKey}/master.m3u8?jwt={jwt}
Secure Thumbnails
When a video is secure, so is a thumbnail. Similar to videos, when referencing a private thumbnail, you must pass through the key.
Whereas before you may have used a public thumbnail URL like so:
<video id="video" controls poster="https://worker.antcdn.net/v1/thumbnail/{orgID}/{envID}/{edgeKey}.png?width=1280&timeStamp=30"></video>Similarly to the video process, add your key as a URL parameter.
<!-- Add the key as a URL parameter here ⬇️ --><video id="video" controls poster="https://worker.antcdn.net/v1/thumbnail/{orgID}/{envID}/{edgeKey}.png?width=1280&timeStamp=30&jwt={jwt}"></video>