Skip to content

Secure video playback

By default, public videos can be viewed by anyone with just a video id. If you want to give access only to certain users, you have to make the video private and use signed URLs. After you mark a video private, it can no longer be accessed publicly. Instead, the user will need a signed URL token to watch or download the video.

Public MP4 download

This page covers private videos. For public videos, the MP4 rendition is available at a permanent, shareable URL that needs no signed token — see MP4 downloads.

Common use cases for using signed URLs:

  • Restricting access to only logged in members
  • Allowing access only for a limited time period (e.g., 24 hours)

1. Making videos require signed URLs

Only private videos require signed URLs. You can set a video to private either through the GUI or the edit video ⧉ API endpoint.

2. Creating signing keys

Next, we'll need to create signing keys. They are used to generate JWT tokens to access private videos.

Signing keys can be managed from the Signing keys ⧉ API endpoints.

Note

Signing keys are different from API keys.

3. Generate JWT tokens

Tokens are generated on the client side using open source libraries. This way, you do not need to call a HeapStream endpoint each time you need to generate a token.

Every token must meet these requirements (the code examples below satisfy them all):

  • Algorithm — tokens are signed with RS256.
  • kid header — set the JWT kid header to the signing key's id, so HeapStream knows which key verifies the token.
  • Private key — the API returns the signing key's private key base64-encoded; decode it before signing.
  • Expiry — keep tokens short-lived: set exp to the shortest lifetime that works for your use case (e.g., a few hours).
Claim code Description Value
sub Subject of the JWT video.id
aud Audience (intended application of the token) v for access to all endpoints
exp Expiration time UNIX Epoch seconds when the token expires
nbf Not Before value UNIX Epoch seconds before which the token will not work

Code examples

# /// script
# requires-python = ">=3.14"
# dependencies = [
#     "pyjwt",
# ]
# ///
import base64
import time

import jwt

video_id = ""  # Enter your video id here
signing_key_id = ""  # Enter your signing key id here
private_key_base64_encoded_from_api = ""  # Enter your base64 encoded private key here


private_key = base64.b64decode(private_key_base64_encoded_from_api)

token = {
    "sub": video_id,
    "aud": "v",
    "exp": int(time.time()) + 3600,
}
headers = {"kid": signing_key_id}

json_web_token = jwt.encode(
    token,
    private_key,
    algorithm="RS256",
    headers=headers,
)

print(json_web_token)
package main

import (
    "encoding/base64"
    "fmt"
    "log"
    "time"

    "github.com/golang-jwt/jwt/v4"
)

func main() {
    // Your signing key id
    keyId := ""
    // Your base64 encoded private key
    base64privateKey := ""
    // Your video_id
    videoId := ""

    decodedKey, err := base64.StdEncoding.DecodeString(base64privateKey)
    if err != nil {
        log.Fatalf("Can not base64 decode private key: %v", err)
    }

    signKey, err := jwt.ParseRSAPrivateKeyFromPEM(decodedKey)
    if err != nil {
        log.Fatalf("Can not parse RSA private key: %v", err)
    }

    token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
        "sub": videoId,
        "aud": "v",
        "exp": time.Now().Add(time.Hour * 15).Unix(),
        "kid": keyId,
    })

    tokenString, err := token.SignedString(signKey)
    if err != nil {
        log.Fatalf("Can not generate token: %v", err)
    }

    fmt.Println(tokenString)
}

Include the JWT token in the URL

A list of URLs that accept the JWT token. Note that each URL names the project in its path — the video is identified by the token itself (its sub claim), so no video id appears in the URL.

Private video page

This URL will open the video page in full screen:

https://staging.heapstream.com/et/<project_id>/?token=<jwt_token>

You can embed this URL in an iFrame like so:

<iframe src="https://staging.heapstream.com/et/<project_id>/?token=<jwt_token>" width="1280" height="720"
        allowFullScreen
        style="border: none;"
        allow="fullscreen; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"></iframe>

Add &autoplay=1 to the URL to have the player try to play as soon as it loads, the same as on the public embed URL. Note that browsers block autoplay with sound; pair it with a muted player if you need playback to start reliably.

Add &player_id=<player_id> to render the embed with a specific one of the project's players instead of the project's default. This works on the public embed URL too. The player must belong to the project; an unknown id falls back to the default player.

HLS master playlist URL

This URL will point to the master HLS playlist:

https://staging.heapstream.com/mmpl/<project_id>/?token=<jwt_token>

Poster URL

https://staging.heapstream.com/pstv/<project_id>/<poster_id>/?token=<jwt_token>

Text track HLS playlist

https://staging.heapstream.com/mplt/<project_id>/<text_track_id>/?token=<jwt_token>

Preset HLS playlist

https://staging.heapstream.com/mpl/<project_id>/<preset_id>/?token=<jwt_token>

Static MP4 rendition

https://staging.heapstream.com/stcr4/<project_id>/<preset_id>/?token=<jwt_token>

Storyboard VTT playlist

https://staging.heapstream.com/sbt/<project_id>/<storyboard_id>/?token=<jwt_token>

Video original URL

Download the original video upload:

https://staging.heapstream.com/dlo/<project_id>/?token=<jwt_token>

Warning

Project keep_originals setting needs to be True (default) for the download to work.