Secure your Stream·续
These values will not be shown again so we recommend saving them securely right away. If you are using Cloudflare Workers, you can store them using Secrets. If you are using another platform, store them in secure environment variables.
You will use these values later to generate the tokens. The pem and jwk fields are base64-encoded, you must decode them before using them (an example of this is shown in step 2).
Step 2: Generate tokens using the key
Once you generate the key in step 1, you can use the pem or jwk values to generate self-signing URLs on your own. Using this method, you do not need to call the Stream API each time you are creating a new token.
Here's an example Cloudflare Worker script which generates tokens that expire in 60 minutes and only work for users accessing the video from UK. In lines 2 and 3, you will configure the id and jwk values from step 1:
// Global variables
const jwkKey = "{PRIVATE-KEY-IN-JWK-FORMAT}";
const keyID = "<KEY_ID>";
const videoUID = "<VIDEO_UID>";
// expiresTimeInS is the expired time in second of the video
const expiresTimeInS = 3600;
// Main function
async function streamSignedUrl() {
const encoder = new TextEncoder();
const expiresIn = Math.floor(Date.now() / 1000) + expiresTimeInS;
const headers = {
alg: "RS256",
kid: keyID,
};
const data = {
sub: videoUID,
kid: keyID,
exp: expiresIn,
// Add `downloadable` boolean for access to MP4 or Audio Downloads:
// downloadable: true,
accessRules: [
{
type: "ip.geoip.country",
action: "allow",
country: ["GB"],
},
{
type: "any",
action: "block",
},
],
};
const token = `${objectToBase64url(headers)}.${objectToBase64url(data)}`;
const jwk = JSON.parse(atob(jwkKey));
const key = await crypto.subtle.importKey(
"jwk",
jwk,
{
name: "RSASSA-PKCS1-v1_5",
hash: "SHA-256",
},
false,
["sign"],
);
const signature = await crypto.subtle.sign(
{ name: "RSASSA-PKCS1-v1_5" },
key,
encoder.encode(token),
);
const signedToken = `${token}.${arrayBufferToBase64Url(signature)}`;
return signedToken;
}
// Utilities functions
function arrayBufferToBase64Url(buffer) {
return btoa(String.fromCharCode(...new Uint8Array(buffer)))
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
}
function objectToBase64url(payload) {
return arrayBufferToBase64Url(
new TextEncoder().encode(JSON.stringify(payload)),
);
}
Step 3: Rendering the video
If you are using the Stream Player, insert the token value returned by the Worker in Step 2 in place of the video id, replacing the entire string located between cloudflarestream.com/ and /iframe:
<iframe
src="https://customer-<CODE>.cloudflarestream.com/eyJhbGciOiJSUzI1NiIsImtpZCI6ImNkYzkzNTk4MmY4MDc1ZjJlZjk2MTA2ZDg1ZmNkODM4In0.eyJraWQiOiJjZGM5MzU5ODJmODA3NWYyZWY5NjEwNmQ4NWZjZDgzOCIsImV4cCI6IjE2MjE4ODk2NTciLCJuYmYiOiIxNjIxODgyNDU3In0.iHGMvwOh2-SuqUG7kp2GeLXyKvMavP-I2rYCni9odNwms7imW429bM2tKs3G9INms8gSc7fzm8hNEYWOhGHWRBaaCs3U9H4DRWaFOvn0sJWLBitGuF_YaZM5O6fqJPTAwhgFKdikyk9zVzHrIJ0PfBL0NsTgwDxLkJjEAEULQJpiQU1DNm0w5ctasdbw77YtDwdZ01g924Dm6jIsWolW0Ic0AevCLyVdg501Ki9hSF7kYST0egcll47jmoMMni7ujQCJI1XEAOas32DdjnMvU8vXrYbaHk1m1oXlm319rDYghOHed9kr293KM7ivtZNlhYceSzOpyAmqNFS7mearyQ/iframe"
style="border: none;"
height="720"
width="1280"
allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;"
allowfullscreen="true"
></iframe>
If you are using your own player, replace the video id in the manifest url with the token value:
https://customer-<CODE>.cloudflarestream.com/eyJhbGciOiJSUzI1NiIsImtpZCI6ImNkYzkzNTk4MmY4MDc1ZjJlZjk2MTA2ZDg1ZmNkODM4In0.eyJraWQiOiJjZGM5MzU5ODJmODA3NWYyZWY5NjEwNmQ4NWZjZDgzOCIsImV4cCI6IjE2MjE4ODk2NTciLCJuYmYiOiIxNjIxODgyNDU3In0.iHGMvwOh2-SuqUG7kp2GeLXyKvMavP-I2rYCni9odNwms7imW429bM2tKs3G9INms8gSc7fzm8hNEYWOhGHWRBaaCs3U9H4DRWaFOvn0sJWLBitGuF_YaZM5O6fqJPTAwhgFKdikyk9zVzHrIJ0PfBL0NsTgwDxLkJjEAEULQJpiQU1DNm0w5ctasdbw77YtDwdZ01g924Dm6jIsWolW0Ic0AevCLyVdg501Ki9hSF7kYST0egcll47jmoMMni7ujQCJI1XEAOas32DdjnMvU8vXrYbaHk1m1oXlm319rDYghOHed9kr293KM7ivtZNlhYceSzOpyAmqNFS7mearyQ/manifest/video.m3u8
To allow access to MP4 or audio downloads, make sure the video has the download type already enabled. Then add downloadable: true to the payload as shown in the comment above when generating the signed URL. Replace the video id in the download URL with the token value:
https://customer-<CODE>.cloudflarestream.com/eyJhbGciOiJ.../downloads/default.mp4
Revoking keys
You can create up to 1,000 keys and rotate them at your convenience. Once revoked all tokens created with that key will be invalidated.
curl --request DELETE \
"https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/keys/{key_id}" \
--header "Authorization: Bearer <API_TOKEN>"
# Response:
{
"result": "Revoked",
"success": true,
"errors": [],
"messages": []
}
Supported Restrictions
| Property Name | Description | |
|---|---|---|
| exp | Expiration. A unix epoch timestamp after which the token will stop working. Cannot be greater than 24 hours in the future from when the token is signed | |
| nbf | Not Before value. A unix epoch timestamp before which the token will not work | |
| downloadable | if true, the token can be used to download the mp4 (assuming the video has downloads enabled) | |
| accessRules | An array that specifies one or more ip and geo restrictions. accessRules are evaluated first-to-last. If a Rule matches, the associated action is applied and no further rules are evaluated. A token may have at most 5 members in the accessRules array. |
accessRules Schema
Each accessRule must include 2 required properties:
type: supported values areany,ip.srcandip.geoip.countryaction: support values areallowandblock
Depending on the rule type, accessRules support 2 additional properties:
country: an array of 2-letter country codes in ISO 3166-1 Alpha 2 ↗︎ format.ip: an array of ip ranges. It is recommended to include both IPv4 and IPv6 variants in a rule if possible. Having only a single variant in a rule means that rule will ignore the other variant. For example, an IPv4-based rule will never be applicable to a viewer connecting from an IPv6 address. CIDRs should be preferred over specific IP addresses. Some devices, such as mobile, may change their IP over the course of a view. Video Access Control are evaluated continuously while a video is being viewed. As a result, overly strict IP rules may disrupt playback.
Example 1: Block views from a specific country
...
"accessRules": [
{
"type": "ip.geoip.country",
"action": "block",
"country": ["US", "DE", "MX"],
},
]
The first rule matches on country, US, DE, and MX here. When that rule matches, the block action will have the token considered invalid. If the first rule doesn't match, there are no further rules to evaluate. The behavior in this situation is to consider the token valid.
Example 2: Allow only views from specific country or IPs
...
"accessRules": [
{
"type": "ip.geoip.country",
"country": ["US", "MX"],
"action": "allow",
},
{
"type": "ip.src",
"ip": ["93.184.216.0/24", "2400:cb00::/32"],
"action": "allow",
},
{
"type": "any",
"action": "block",
},
]
The first rule matches on country, US and MX here. When that rule matches, the allow action will have the token considered valid. If it doesn't match we continue evaluating rules
The second rule is an IP rule matching on CIDRs, 93.184.216.0/24 and 2400:cb00::/32. When that rule matches, the allow action will consider the rule valid.
If the first two rules don't match, the final rule of any will match all remaining requests and block those views.
Security considerations
Hotlinking Protection
By default, Stream embed codes can be used on any domain. If needed, you can limit the domains a video can be embedded on from the Stream dashboard.
In the dashboard, you will see a text box by each video labeled Enter allowed origin domains separated by commas. If you click on it, you can list the domains that the Stream embed code should be able to be used on. `
*.badtortilla.comcoversa.badtortilla.com,a.b.badtortilla.comand does not coverbadtortilla.comexample.comdoes not cover www.example.com ↗︎ or any subdomain of example.comlocalhostrequires a port if it is not being served over HTTP on port 80 or over HTTPS on port 443- There is no path support -
example.comcoversexample.com/\*
You can also control embed limitation programmatically using the Stream API. uid in the example below refers to the video id.
curl https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/{video_uid} \
--header "Authorization: Bearer <API_TOKEN>" \
--data "{\"uid\": \"<VIDEO_UID>\", \"allowedOrigins\": [\"example.com\"]}"
You can also set allowed origins using the Stream binding:
export default {
async fetch(request, env) {
const video = await env.STREAM.video("VIDEO_ID").update({
allowedOrigins: ["example.com"],
});
return Response.json(video);
},
};
export default {
async fetch(request, env) {
const video = await env.STREAM.video("VIDEO_ID").update({
allowedOrigins: ["example.com"],
});
return Response.json(video);
},
};
Allowed Origins
The Allowed Origins feature lets you specify which origins are allowed for playback. This feature works even if you are using your own video player. When using your own video player, Allowed Origins restricts which domain the HLS/DASH manifests and the video segments can be requested from.
Signed URLs
Combining signed URLs with embedding restrictions allows you to strongly control how your videos are viewed. This lets you serve only trusted users while preventing the signed URL from being hosted on an unknown site.
Was this helpful?
YesNo
On this page
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/stream/viewing-videos/securing-your-stream/#page","headline":"Secure your Stream","description":"Restrict access to Cloudflare Stream videos using signed URLs and tokens.","url":"https://developers.cloudflare.com/stream/viewing-videos/securing-your-stream/","inLanguage":"en","image":"https://developers.cloudflare.com/stream/viewing-videos/securing-your-stream/og.png?v=de718c2fda3a6680","dateModified":"2026-05-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
description: Play Cloudflare Stream videos with any HLS/DASH-compatible player on web, iOS, or Android. title: Use your own player image: https://developers.cloudflare.com/stream/viewing-videos/using-own-player/og.png?v=7f7bfaf5f1d9fbb9
Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/stream/llms.txt
Use this file to discover all available pages before exploring further.
Docs