# Put video behind a paywall

Gate Mux video for subscribers, purchasers, or members with AI agents. Get signed playback, a token endpoint tied to your billing, optional free previews, and DRM for premium content.

Source: https://www.mux.com/docs/prompts/video-paywall.md · Last updated: 2026-10-02

## Agent instructions

Use this procedure to put Mux video behind a paywall: build paid or members-only playback into an application, protect videos that are currently public, or audit an existing gated integration. Your application decides who has paid. Mux enforces that decision through signed playback: each video uses a `signed` playback policy, and your server issues a short-lived JSON Web Token (JWT) only to viewers who are entitled to watch. DRM is an optional additional layer for premium content.

Follow the user's requirements and existing project instructions. Reuse the tools, credentials, authentication, and billing integration already available. If a Mux MCP server is already connected, use it for Mux operations. If the environment has no Mux credentials yet and you can run shell commands, sign in with the [Mux CLI](https://www.mux.com/docs/integrations/mux-cli.md): run `npx @mux/cli login`, or `mux login` if it is installed. It opens a browser where the user approves access and chooses the Mux environment, so nobody creates or pastes an access token. Agent shells and `!` commands are not interactive terminals, where a plain `login` exits with an error. Run `npx @mux/cli login --json` in the background instead: it writes the authorization URL as a JSON line on stderr and waits up to five minutes for the user to approve. Give the user that URL and wait for the command to finish. Without a shell, fetch https://www.mux.com/prompts/onboarding.md and complete it first; it connects the Mux MCP server. The examples and defaults below are starting choices; adapt them to the request.

Canonical instructions: [/docs/prompts/video-paywall.md](https://www.mux.com/docs/prompts/video-paywall.md). Browse other use cases in the prompt library at [/docs/prompts.md](https://www.mux.com/docs/prompts.md). For repeated use in an agent that supports skills, use [/skills/mux-video-paywall/SKILL.md](https://www.mux.com/skills/mux-video-paywall/SKILL.md) as the entry point.

## Track your progress with this checklist

Copy this checklist into your task list or plan before starting, and keep it updated as you work. Mark each item done, skipped with the reason, or blocked with what is needed. Include the final state of the checklist in your report to the user.

1. Identify the mode: build an application, protect existing videos, or audit an existing integration.
2. Check the environment for Mux credentials and tools. If there are none, sign in with the Mux CLI as described above. If you cannot check, say so and offer the CLI sign-in rather than asking the user for an access token.
3. Resolve every question in the table under **Start with the request**: answered by the user, answered from the project, or not applicable with the reason.
4. Choose the protection level and state why, including whether DRM or playback restrictions are needed.
5. Complete each numbered step that applies, or record why it was skipped.
6. Run every check under **Verify the result** that applies, against a real video where credentials allow.
7. Report which checks ran against real Mux and billing state, and which still need credentials, a test purchase, or target devices.

## Start with the request

First determine whether the user wants to **build an application** with gated playback, **protect existing videos** that are already in Mux, or **audit an existing integration**. A codebase is needed for the first mode. The second can run against the Mux API alone, though viewers still need a server that issues tokens before they can watch.

Inspect the available project, its authentication and billing code, and configured tools before asking questions. Use what the user has already told you, and ask only for missing choices that change the work:

| Question | Why it matters |
| :-- | :-- |
| How do viewers pay: subscription, one-time purchase, rental, or membership tier? | Defines the entitlement check and how long access should last. |
| Where does the record of who has paid live: a billing provider, your database, or an auth provider? | The token endpoint reads this record. Mux does not process payments or store entitlements. |
| Is the content on-demand, live, or both? Are the videos already in Mux with public playback IDs? | Determines whether to create new signed assets, migrate existing ones, or configure live streams and their recordings. |
| Where do viewers watch: web, native iOS or Android apps, TVs, casting? | Affects the player integration and how playback restrictions treat requests without a `Referer` header. |
| Should non-paying visitors see a free preview, poster image, or nothing? | Decides whether to issue range-limited preview tokens and signed thumbnail tokens to anonymous visitors. |
| How strong does protection need to be? Is screen recording or downloading a concern? | Signed playback controls who can start a stream. DRM adds encryption and capture protection, with an onboarding process and additional cost. |

Treat payment processing, account management, and entitlement storage as the application's responsibility. Use the project's existing providers. When none exists, ask the user which to use rather than choosing one for them.

### Choose an execution method

| Available capability | How to use it |
| :-- | :-- |
| Repository and coding tools | Add the token endpoint, player integration, and webhook handling to the existing application's auth, billing, and data conventions. |
| Mux MCP, CLI, or API access | Create signing keys, playback restrictions, and signed playback IDs; migrate existing assets; verify that unsigned requests are denied. Check which operations the installed tool version supports. Create signing keys with the CLI or the dashboard rather than the MCP server: a key created through MCP returns its private key into the conversation. With the MCP server's `execute` tool, check job and asset status in separate short calls instead of a wait loop inside one call: agents may move a long-running tool call to the background and leave it there until it finishes. |
| Browser or computer use | Use the dashboard for signing keys and DRM settings, and a browser to test playback as a paying and a non-paying viewer. Record IDs and verify the resulting state after each change. |
| Advice-only chat | Produce an implementation plan and handoff instructions. Identify the credentials, secrets storage, and server environment needed to run it. |

If a needed operation is unavailable, explain the handoff rather than inventing a tool. Never place a signing key's private key in client code, a repository, or a chat transcript.

## Choose a protection level

| Requirement | Approach |
| :-- | :-- |
| Only paying viewers can start playback | Use a `signed` playback policy and issue playback JWTs from an endpoint behind your entitlement check. |
| Playback only from your own sites or apps | Add a [playback restriction](https://www.mux.com/docs/guides/secure-video-playback.md#3-create-an-optional-playback-restriction-for-your-mux-account-environment) and reference its ID in each token. |
| A teaser for visitors who have not paid | Issue a token whose claims limit playback to a time range of the same signed video. |
| Resistance to screen recording and download tools | Add [DRM](https://www.mux.com/docs/guides/protect-videos-with-drm.md) on top of signed playback. |
| Tracing a leaked URL back to a session | Put a non-identifying session reference in the token's `custom` claim. |

Signed playback is the baseline for every paywall. A valid token works for anyone who holds it until it expires, so expiration time, playback restrictions, and DRM determine how far a shared token can travel. Start with signed playback and add layers when the content's value calls for them.

## 1. Create a signing key and store it on the server

[Signing keys](https://www.mux.com/docs/guides/secure-video-playback.md#2-create-a-signing-key-for-your-mux-account-environment) are separate from API access tokens. With the Mux CLI signed in, create one with:

```bash
npx @mux/cli signing-keys create
```

The CLI saves the key ID and private key to its own configuration for the signed-in environment and does not print the private key. `npx @mux/cli sign YOUR_SIGNED_PLAYBACK_ID` then signs test URLs with it, which is enough to verify Mux's enforcement. The application's server needs the same two values as `MUX_SIGNING_KEY` and `MUX_PRIVATE_KEY`: they are stored as `signingKeyId` and `signingPrivateKey` for the environment in `~/.config/mux/config.json`. Ask the user to copy them into the server's secret store, such as `.env.local`, instead of reading or printing the private key yourself.

Without the CLI, create a key from the [Signing Keys settings](https://dashboard.mux.com/settings/signing-keys) in the dashboard, or with the API:

```bash
curl -X POST https://api.mux.com/system/v1/signing-keys \
  --user "$MUX_TOKEN_ID:$MUX_TOKEN_SECRET"
```

The response contains the key `id` and a base64-encoded `private_key`. Mux keeps only the public key, and later requests for the signing key do not return the private key, so save it to the server's secret store as soon as it is returned. The [Mux TypeScript SDK](https://www.mux.com/docs/integrations/mux-typescript-sdk.md) (`@mux/ts`) reads these values from `MUX_SIGNING_KEY` (the key ID) and `MUX_PRIVATE_KEY` (the base64-encoded private key). Signing keys belong to one Mux environment; use a key from the same environment as the videos.

Creating a signing key through the API needs an access token with **System** write permission, and creating assets needs **Mux Video** write. A CLI sign-in covers both. The server that issues playback tokens needs neither: the SDK signs tokens locally, so `new Mux()` works there with only `MUX_SIGNING_KEY` and `MUX_PRIVATE_KEY` set. Keep the access token out of that server unless it also calls the API.

One active key is usually enough. To rotate, create a new key, switch the server to it, and delete the old key only after tokens signed with it have expired.

## 2. Give each video a signed playback ID

A video's playback policy determines whether a token is required. An asset created without any playback policy cannot be played at all until a playback ID is added.

**New on-demand videos.** Set the policy when creating the asset. For [direct uploads](https://www.mux.com/docs/guides/upload-files-directly.md), set the same field inside `new_asset_settings`. With the CLI:

```bash
npx @mux/cli assets create --url https://example.com/your-video.mp4 --playback-policy signed --video-quality basic --wait --json
```

Or with the API:

```bash
curl https://api.mux.com/video/v1/assets \
  --user "$MUX_TOKEN_ID:$MUX_TOKEN_SECRET" \
  --header "Content-Type: application/json" \
  --data '{
    "inputs": [{ "url": "https://example.com/your-video.mp4" }],
    "playback_policies": ["signed"],
    "video_quality": "basic"
  }'
```

Use `basic` video quality as a starting point; choose a different [quality level](https://www.mux.com/docs/guides/use-video-quality-levels.md) if the content needs it. Wait for `video.asset.ready` before offering playback.

**Existing videos with public playback IDs.** Add a signed playback ID, move the application to it, then remove the public one:

```bash
curl https://api.mux.com/video/v1/assets/YOUR_ASSET_ID/playback-ids \
  --user "$MUX_TOKEN_ID:$MUX_TOKEN_SECRET" \
  --header "Content-Type: application/json" \
  --data '{ "policy": "signed" }'
```

An asset can hold several playback IDs at once, so both work during the transition. The video remains publicly playable until the public playback ID is deleted with `DELETE /video/v1/assets/{ASSET_ID}/playback-ids/{PLAYBACK_ID}`. Deleting it breaks every existing embed and link that uses that ID, and a viewer who started watching beforehand may be able to continue for a limited time. Confirm with the user before deleting public playback IDs, and migrate in batches with a saved record of each asset's old and new IDs so the work can be resumed.

**Live streams.** Create the live stream with a `signed` playback policy, and set the same policy in `new_asset_settings` so recordings of the stream are gated as well. See [start live streaming](https://www.mux.com/docs/guides/start-live-streaming.md).

Save each video's playback ID and its policy in the application's database. Appending a `token` to a URL for a `public` playback ID makes the request fail, so an application that mixes free and paid videos needs to know which IDs to sign for.

## 3. Add playback restrictions if you need them

A [playback restriction](https://www.mux.com/docs/guides/secure-video-playback.md#3-create-an-optional-playback-restriction-for-your-mux-account-environment) limits which sites or clients can play a signed video. Restrictions exist at the environment level and apply only when a token references one through the `playback_restriction_id` claim.

```bash
curl https://api.mux.com/video/v1/playback-restrictions \
  --user "$MUX_TOKEN_ID:$MUX_TOKEN_SECRET" \
  --header "Content-Type: application/json" \
  --data '{
    "referrer": {
      "allowed_domains": ["example.com", "*.example.com"],
      "allow_no_referrer": false
    },
    "user_agent": {
      "allow_no_user_agent": false,
      "allow_high_risk_user_agent": false
    }
  }'
```

Save the returned `id`. Account for these documented behaviors before enabling a restriction:

* A wildcard covers one subdomain level: `*.example.com` does not match `example.com` or `a.b.example.com`. List each pattern you need.
* Native iOS and Android apps do not send a `Referer` header. Create a second restriction with `allow_no_referrer: true` for native clients, and reference the appropriate restriction ID in tokens for web versus native viewers.
* Chromecast needs `www.gstatic.com` in the allowed domains. AirPlay to third-party devices needs `mediaservices.cdn-apple.com`, and first-party Apple devices never send a referrer, so they require `allow_no_referrer: true`.
* Include local development and preview domains in a separate restriction for non-production use, so that the production allow list stays narrow.

## 4. Issue tokens from an entitlement-checked endpoint

Build a server endpoint that authenticates the viewer, checks their entitlement to the requested video, and only then signs tokens. This check is the paywall. Do it on every token request, against current billing state, and look up the playback ID on the server from your own video record rather than signing whatever ID the client sends.

Each protected resource needs its own token, distinguished by the `aud` claim:

| Resource | `aud` | Used for |
| :-- | :-- | :-- |
| Playback | `v` | The video stream and its captions |
| Thumbnail | `t` | The poster image |
| Storyboard | `s` | Timeline hover previews for on-demand video |
| Animated GIF | `g` | GIF previews |
| DRM license | `d` | License requests for DRM-protected playback |

Every token carries `sub` (the playback ID), `aud`, `exp` (expiration in UNIX epoch seconds), and `kid` (the signing key ID), and is signed with the RS256 algorithm. With the Mux TypeScript SDK, one call can return the playback, thumbnail, and storyboard tokens:

```javascript
import Mux from '@mux/ts';

// Reads MUX_SIGNING_KEY and MUX_PRIVATE_KEY from the environment
const mux = new Mux();

const tokens = await mux.jwt.signPlaybackId(playbackId, {
  type: ['video', 'thumbnail', 'storyboard'],
  expiration: '6h',
  params: { playback_restriction_id: PLAYBACK_RESTRICTION_ID },
});
// { 'playback-token': '...', 'thumbnail-token': '...', 'storyboard-token': '...' }
```

Omit `params` when no playback restriction is used. For other languages, see the examples in [signing JWTs](https://www.mux.com/docs/guides/signing-jwts.md#sign-video-playback-jwts); any JWT library that supports RS256 works.

**Choose the expiration deliberately.** When a token expires the URL stops working, even if playback has already started. Set `exp` to at least the current time plus the video's duration, or the expected length of a live stream. The `6h` above is an illustrative value. Shorter lifetimes limit how long a shared token stays useful. Fetch new tokens when a viewer returns to a page after a long absence, and consider tying expiration to the end of a rental or billing period when access should end at a known time.

**Options belong in the claims.** For a signed playback ID, thumbnail options such as `time` and `width`, and playback modifiers such as `default_subtitles_lang`, must be set as claims in the token. The final URL should contain only the `token` query parameter. Parameters added next to the token are not covered by the signature, so do not rely on Mux either applying or rejecting them.

**Trace shared tokens without personal data.** A `custom` claim can carry a session reference that lets you trace a leaked URL to the account that requested it. Never put names, email addresses, or other personally identifiable information in a token.

```json
{
  "sub": "YOUR_PLAYBACK_ID",
  "aud": "v",
  "exp": 1790000000,
  "kid": "YOUR_SIGNING_KEY_ID",
  "custom": { "session_id": "xxxx-123" }
}
```

### Keep entitlements current

Access ends when the endpoint stops issuing tokens, so the entitlement record has to follow billing changes: cancellations, failed payments, refunds, and expired rentals. Update it from the billing provider's verified events, using the project's existing integration. A token that was already issued remains valid until its `exp`; choose lifetimes with that in mind. For an emergency removal, deleting the playback ID cuts off access for everyone, and a new signed playback ID restores it for entitled viewers.

## 5. Play the video with tokens

Use [Video.js 10](https://videojs.org/docs/framework/react/reference/components/mux-video) with its Mux video component. If the project already plays video with another player, keep it and give it the signed URLs instead.

Each token goes in the `source` group for the URL it signs. The playback token goes at `source.playback.token`, the thumbnail token at `source.poster.token`, and the storyboard token at `source.storyboard.token`. Video.js checks the audience of the poster and storyboard tokens and skips the poster or the timeline previews when one is missing or has the wrong audience. Playback tokens pass through to Mux, so a misplaced one shows up as a rejected stream request. In React:

```bash
npm install @videojs/react @videojs/mux-video @videojs/mux-data
```

```tsx
'use client';

import '@videojs/react/video/skin.css';
import { VideoPlayer, VideoSkin } from '@videojs/react/video';
import { MuxData } from '@videojs/react/extensions/mux-data';
import { MuxVideo } from '@videojs/react/media/mux-video';

// tokens is the response from the entitlement-checked token endpoint
export function PaidVideo({ playbackId, tokens }) {
  return (
    <VideoPlayer>
      <VideoSkin style={{ aspectRatio: '16 / 9' }}>
        <MuxVideo
          source={{
            playbackId,
            playback: { token: tokens['playback-token'] },
            poster: { token: tokens['thumbnail-token'] },
            storyboard: { token: tokens['storyboard-token'] },
          }}
          playsInline
          crossOrigin="anonymous"
        />
        <MuxData />
      </VideoSkin>
    </VideoPlayer>
  );
}
```

Without React, install `@videojs/html` instead, import `@videojs/html/video/player`, `@videojs/html/video/skin`, and `@videojs/html/media/mux-video`, and set the same object on the element's `source` property:

```javascript
document.querySelector('mux-video').source = {
  playbackId: 'YOUR_SIGNED_PLAYBACK_ID',
  playback: { token: 'YOUR_PLAYBACK_TOKEN' },
  poster: { token: 'YOUR_THUMBNAIL_TOKEN' },
  storyboard: { token: 'YOUR_STORYBOARD_TOKEN' },
};
```

Follow the Video.js [installation guides](https://videojs.org/docs/guides/installation.md) for the project's framework, and remove any Mux Player or Media Chrome imports from pages that load Video.js, since they register custom elements with the same names. `MuxData` reports playback to Mux Data without an environment key, because Mux attributes views to the environment that owns the playback ID. For other players, append the playback token to the stream URL:

```text
https://stream.mux.com/YOUR_SIGNED_PLAYBACK_ID.m3u8?token=YOUR_PLAYBACK_TOKEN
```

Render the player only after the server has confirmed entitlement. Show non-paying visitors the purchase or sign-in prompt instead, optionally with a signed poster image. Hiding a player with CSS or client-side state is not protection; the absence of a valid token is.

The common token problems are a playback ID that does not match the token's `sub`, an expired token, and a malformed token. Video.js does not refresh tokens or report expired ones yet, so the application has to request fresh tokens before `exp`, retry after a [playback error](https://videojs.org/docs/framework/react/guides/playback-errors), and show the paywall when the endpoint refuses. Assigning a new `source` reloads the stream, so save the current time before swapping tokens and seek back to it once the new source has loaded metadata. Avoid swapping tokens on a stream that is playing without errors.

If the application offers [downloadable MP4 files](https://www.mux.com/docs/guides/enable-static-mp4-renditions.md#signed-static-rendition-urls) for a signed playback ID, those requests need signed URLs too. Generate them on demand behind the same entitlement check rather than storing them as permanent links.

### Offer a free preview

A preview can come from the same signed video, without a second asset. Issue anonymous visitors a playback token whose claims include `asset_start_time` and `asset_end_time`, as described in [signed instant clips](https://www.mux.com/docs/guides/create-instant-clips.md#via-signed-urls). Because the range is part of the signed claims, the viewer cannot widen it. Add the same claims to the storyboard token, and use a thumbnail token for the poster. With the SDK, pass per-type claims as `[type, params]` pairs:

```javascript
const clip = { asset_start_time: 0, asset_end_time: 30 };
const previewTokens = await mux.jwt.signPlaybackId(playbackId, {
  type: [['video', clip], ['storyboard', clip], 'thumbnail'],
  expiration: '15m',
});
```

The SDK's TypeScript types declare `params` values as strings, so TypeScript projects need a cast to pass these numbers. Instant clipping uses segment-level boundaries, so the preview may start or end slightly outside the requested times. Keep the range well short of the video's duration, and derive it from the asset's `duration` rather than a fixed number: a 30-second preview of a 25-second video is the whole video. Rate-limit the preview endpoint, since it serves unauthenticated visitors.

## 6. Add DRM for premium content

Signed playback controls who can start a stream. [DRM](https://www.mux.com/docs/guides/protect-videos-with-drm.md) adds video encryption, screen capture protection on supporting devices, and HDCP on Apple devices. Protection varies by device: desktop browsers typically rely on software decryption, where capture blocking is less dependable. Recommend DRM when the user's content or licensing terms require it, and state these prerequisites plainly:

* DRM is an add-on with a monthly access fee and a per-license charge; see [DRM pricing](https://www.mux.com/docs/guides/protect-videos-with-drm.md#pricing). One license request typically corresponds to one view.
* Access is requested from **Settings → Digital Rights Management** in the dashboard, after which the environment receives a DRM configuration ID.
* Playback on Apple devices requires the user's own FairPlay certificate from Apple. Approval can take several days. Widevine and PlayReady are managed by Mux.
* DRM is supported on the `plus` and `premium` video quality levels.

Create DRM-protected assets with `advanced_playback_policies`. This field cannot be combined with `playback_policies` in the same request; include multiple entries in the array when more than one policy is needed.

```json
{
  "inputs": [{ "url": "https://example.com/your-video.mp4" }],
  "advanced_playback_policies": [
    { "policy": "drm", "drm_configuration_id": "YOUR_DRM_CONFIGURATION_ID" }
  ],
  "video_quality": "plus"
}
```

A DRM playback ID can also be added to an existing asset through the playback IDs endpoint, for assets created after DRM was enabled in the environment. Playback needs both a playback token and a DRM license token, issued by the same entitlement-checked endpoint:

```javascript
const playbackToken = await mux.jwt.signPlaybackId(playbackId, { expiration: '6h' });
const drmLicenseToken = await mux.jwt.signDrmLicense(playbackId, { expiration: '6h' });
```

In Video.js, put the license token at `source.drm.token`, next to `source.playback.token`. Video.js derives the FairPlay, Widevine, and PlayReady license servers and the FairPlay certificate from it; verify playback with the protected assets on every target browser. In native apps, Mux Player for iOS and Android supports DRM from version 1.1.0. For other players, build the license URLs described in the DRM guide. If DRM onboarding is still pending, ship signed playback first and add the DRM playback IDs afterward.

Visible [watermarks](https://www.mux.com/docs/guides/add-watermarks-to-your-videos.md) are encoded into the video and are the same for every viewer. Per-viewer overlays drawn by the player are easier to bypass, and forensic watermarking is not currently available.

## Audit an existing paywall

Use this section when the user already gates video and wants to know whether it holds. Work read-only until the user approves changes, and report findings with the asset or file they refer to.

1. List the gated videos from the application's records and retrieve each asset's playback IDs. Flag any gated asset that still has a `public` playback ID, and any gated live stream whose `new_asset_settings` would publish recordings publicly.
2. Read the token endpoint. Confirm that it authenticates the viewer, checks entitlement for the specific video against current billing state, and resolves the playback ID on the server. An endpoint that signs any ID a client supplies is a bypass.
3. Search the client bundle, repository history, environment files, and logs for the signing key's private key. If it has been exposed, rotate the key as described in step 1.
4. Review token lifetimes against video durations and the payment model, and check that billing events such as cancellations and refunds update the entitlement record.
5. Check playback restrictions against the clients in use, then run the checks under **Verify the result**.

## Verify the result

Before calling the paywall complete, check that:

* Requesting `https://stream.mux.com/YOUR_SIGNED_PLAYBACK_ID.m3u8` without a token is denied, and so are requests with an expired token and with a thumbnail token in place of the playback token. The SDK will not sign a token that is already expired; sign one with a short expiration and wait for it to pass, or sign the expired test token with a JWT library.
* The token endpoint refuses unauthenticated and non-entitled viewers, and cannot be made to sign a playback ID the viewer has not paid for.
* An entitled viewer can play the video, see the poster and timeline previews, and resume after returning to the page later.
* Cancelling or refunding a test purchase stops new tokens from being issued.
* No gated video still has a public playback ID, unless the user chose to keep one. List each asset's playback IDs to confirm.
* The signing key's private key exists only in the server's secret store: not in client bundles, the repository, logs, or API responses.
* Playback restrictions allow every intended client, including native apps, casting targets, and development domains, and deny an origin that is not listed.
* With DRM, playback works on each target platform, including an Apple device once the FairPlay certificate is in place.
* A preview token plays only its range, and the full video remains unavailable to the same visitor.

For an agent-built implementation, report which of these were tested against a real video and billing state, and which still need credentials, a test purchase, or target devices. Local checks alone do not establish that the paywall holds in production.

## Understand the costs

The workflow incurs the usual encoding, storage, and delivery charges for each video. DRM adds a monthly access fee and per-license charges, and requires `plus` or `premium` video quality. Migrating an existing video adds a playback ID to the same asset rather than creating another one. Use the [pricing overview](https://www.mux.com/docs/pricing/overview.md) and [cost-estimation guide](https://www.mux.com/docs/pricing/estimating-video-costs.md) with your library size and expected viewing.
