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.
Give this to your agent
Paste this into your AI agent. It fetches the current instructions, asks you about anything it still needs to know, and does the work in this session.
Fetch https://www.mux.com/docs/prompts/video-paywall.md?ref=prompt and follow the instructions. If you can't fetch URLs, tell me and I'll paste the instructions instead.
Give this prompt to your agent to add paid or members-only video to an application, protect a library that is currently public, or check an existing paywall for gaps. A useful starting brief is: “Only active subscribers should be able to watch our course videos. Everyone else sees a two-minute preview and a prompt to subscribe.”
Your application decides who has paid; Mux enforces it. Each gated video uses a signed playback policy, which means a stream only plays with a valid JSON Web Token. When a viewer opens a video, your server confirms their entitlement and signs a short-lived token for that video. Visitors without one cannot start playback, even if they have the playback ID.
The instructions connect secure video playback, JWT signing, Video.js signed playback, and DRM, including the migration steps for videos that are already public.
| Input | What to provide |
|---|---|
| Your payment model | Subscription, one-time purchase, rental, or membership tiers, and how long access lasts. |
| Your record of who has paid | The billing provider, database, or auth provider that knows each viewer's entitlement. |
| Your videos | New uploads, existing Mux assets, live streams, or a mix; note which are public today. |
| Where viewers watch | Web, native apps, TVs, or casting; include the domains that should be allowed to play. |
| Preview and protection choices | Whether non-paying visitors get a teaser, and whether screen recording is a concern. |
| An execution environment | A project where the agent can build the integration, with access to the necessary Mux tools. |
Your agent uses details already available in your project or conversation and asks only about the missing choices. It can work through API or CLI tools, MCP, or supported dashboard operations. Payments and accounts stay with the providers your project already uses.
For an existing library, the agent adds a signed playback ID to each video, moves your application over, and removes the public ID once you confirm. Both IDs work during the transition, so viewers are not interrupted. Removing a public playback ID breaks any embeds or links that still use it; the instructions have the agent confirm before deleting and keep a record of each video's old and new IDs. The reusable skill gives a compatible agent the same instructions for later additions to the library.
Signed playback controls who can start a stream. A valid token works for anyone who holds it until it expires, so token lifetime and domain restrictions determine how far a shared link can travel. No streaming protection prevents every form of copying. DRM adds encryption and screen capture protection on supporting devices; it carries a monthly fee and per-license charges, needs the plus or premium quality level, and requires your own FairPlay certificate from Apple for playback on Apple devices.
Mux does not process payments or track entitlements. Your application remains responsible for billing, accounts, and updating access when a subscription ends or a purchase is refunded. See the pricing overview and DRM pricing.
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: 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. Browse other use cases in the prompt library at /docs/prompts.md. For repeated use in an agent that supports skills, use /skills/mux-video-paywall/SKILL.md as the entry point.
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.
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.
| 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.
| 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 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 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.
Signing keys are separate from API access tokens. With the Mux CLI signed in, create one with:
npx @mux/cli signing-keys createThe 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 in the dashboard, or with the API:
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 (@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.
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, set the same field inside new_asset_settings. With the CLI:
npx @mux/cli assets create --url https://example.com/your-video.mp4 --playback-policy signed --video-quality basic --wait --jsonOr with the API:
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 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:
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.
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.
A playback restriction 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.
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:
*.example.com does not match example.com or a.b.example.com. List each pattern you need.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.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.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:
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; 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.
{
"sub": "YOUR_PLAYBACK_ID",
"aud": "v",
"exp": 1790000000,
"kid": "YOUR_SIGNING_KEY_ID",
"custom": { "session_id": "xxxx-123" }
}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.
Use Video.js 10 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:
npm install @videojs/react @videojs/mux-video @videojs/mux-data'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:
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 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:
https://stream.mux.com/YOUR_SIGNED_PLAYBACK_ID.m3u8?token=YOUR_PLAYBACK_TOKENRender 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, 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 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.
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. 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:
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.
Signed playback controls who can start a stream. DRM 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:
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.
{
"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:
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 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.
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.
public playback ID, and any gated live stream whose new_asset_settings would publish recordings publicly.Before calling the paywall complete, check that:
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.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.
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 and cost-estimation guide with your library size and expected viewing.