---
title: Secure your Stream
description: Restrict access to Cloudflare Stream videos using signed URLs and tokens.
image: https://developers.cloudflare.com/dev-products-preview.png
---

> 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.

[Skip to content](#%5Ftop) 

# Secure your Stream

## Signed URLs / Tokens

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

Here are some common use cases for using signed URLs:

* Restricting access so only logged in members can watch a particular video
* Let users watch your video for a limited time period (ie. 24 hours)
* Restricting access based on geolocation

### Making a video require signed URLs

Turn on `requireSignedURLs` to protect a video using signed URLs. This option will prevent _any public links_, such as `customer-<CODE>.cloudflarestream.com/<VIDEO_ID>/watch` or the built-in player, from working.

Restricting viewing can be done by updating the video's metadata.

Terminal window

```

curl "https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/{video_uid}" \

--header "Authorization: Bearer <API_TOKEN>" \

--header "Content-Type: application/json"

--data "{\"uid\": \"<VIDEO_UID>\", \"requireSignedURLs\": true }"


```

Response:

```

{

  "result": {

    "uid": "<VIDEO_UID>",

    ...

    "requireSignedURLs": true

  },

  "success": true,

  "errors": [],

  "messages": []

}


```

#### Workers binding

You can also require signed URLs using the Stream binding in your Worker. Refer to [Bind to Workers API](https://developers.cloudflare.com/stream/manage-video-library/bindings/) for setup instructions.

* [  JavaScript ](#tab-panel-8483)
* [  TypeScript ](#tab-panel-8484)

JavaScript

```

export default {

  async fetch(request, env) {

    const video = await env.STREAM.video("VIDEO_ID").update({

      requireSignedURLs: true,

    });

    return Response.json(video);

  },

};


```

TypeScript

```

export default {

  async fetch(request, env) {

    const video = await env.STREAM.video("VIDEO_ID").update({

      requireSignedURLs: true,

    });

    return Response.json(video);

  },

};


```

## Three Ways to Generate Signed Tokens

You can program your app to generate tokens in three ways:

* **Low-volume or testing: Use the `/token` endpoint to generate a short-lived signed token.** This is recommended for testing purposes or if you are generating less than 1,000 tokens per day. It requires making an API call to Cloudflare for each token, _which is subject to [rate limiting](https://developers.cloudflare.com/fundamentals/api/reference/limits/)._ The default result is valid for 1 hour. This method does not support [Live WebRTC](https://developers.cloudflare.com/stream/webrtc-beta/).
* **Recommended: Use a signing key to create tokens.** If you have thousands of daily users or need to generate a high volume of tokens, as with [Live WebRTC](https://developers.cloudflare.com/stream/webrtc-beta/), you can create tokens yourself using a signing key. This way, you do not need to call a Stream API each time you need to generate a token, and is therefore _not_ a rate-limited operation.
* **Workers binding: Use the Stream binding to generate tokens.** If you are using Cloudflare Workers with the Stream binding, you can generate tokens directly without making a separate API call or managing signing keys. This is the simplest approach for Workers users. For advanced customization such as access rules or custom expiration, use a signing key instead.

## Option 1: Using the /token endpoint

You can call the `/token` endpoint for any video that is marked private to get a signed URL token which expires in one hour. This method does not support [Live WebRTC](https://developers.cloudflare.com/stream/webrtc-beta/).

Terminal window

```

curl --request POST \

https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/{video_uid}/token \

--header "Authorization: Bearer <API_TOKEN>"


```

You will see a response similar to this if the request succeeds:

```

{

  "result": {

    "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImNkYzkzNTk4MmY4MDc1ZjJlZjk2MTA2ZDg1ZmNkODM4In0.eyJraWQiOiJjZGM5MzU5ODJmODA3NWYyZWY5NjEwNmQ4NWZjZDgzOCIsImV4cCI6IjE2MjE4ODk2NTciLCJuYmYiOiIxNjIxODgyNDU3In0.iHGMvwOh2-SuqUG7kp2GeLXyKvMavP-I2rYCni9odNwms7imW429bM2tKs3G9INms8gSc7fzm8hNEYWOhGHWRBaaCs3U9H4DRWaFOvn0sJWLBitGuF_YaZM5O6fqJPTAwhgFKdikyk9zVzHrIJ0PfBL0NsTgwDxLkJjEAEULQJpiQU1DNm0w5ctasdbw77YtDwdZ01g924Dm6jIsWolW0Ic0AevCLyVdg501Ki9hSF7kYST0egcll47jmoMMni7ujQCJI1XEAOas32DdjnMvU8vXrYbaHk1m1oXlm319rDYghOHed9kr293KM7ivtZNlhYceSzOpyAmqNFS7mearyQ"

  },

  "success": true,

  "errors": [],

  "messages": []

}


```

#### Workers binding

You can generate a signed token using the Stream binding:

* [  JavaScript ](#tab-panel-8485)
* [  TypeScript ](#tab-panel-8486)

JavaScript

```

export default {

  async fetch(request, env) {

    const token = await env.STREAM.video("VIDEO_ID").generateToken();

    return Response.json({ token });

  },

};


```

TypeScript

```

export default {

  async fetch(request, env) {

    const token = await env.STREAM.video("VIDEO_ID").generateToken();

    return Response.json({ token });

  },

};


```

To render the video or use assets like manifests or thumbnails, use the `token` value in place of the video/input ID. For example, to use the Stream player, replace the ID between `cloudflarestream.com/` and `/iframe` with the token: `https://customer-<CODE>.cloudflarestream.com/<TOKEN>/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>


```

Similarly, if you are using your own player, retrieve the HLS or DASH manifest by replacing the video ID in the manifest URL with the `token` value:

* `https://customer-<CODE>.cloudflarestream.com/<TOKEN>/manifest/video.m3u8`
* `https://customer-<CODE>.cloudflarestream.com/<TOKEN>/manifest/video.mpd`

### Customizing default restrictions

If you call the `/token` endpoint without any body, it will return a token that expires in one hour without any other restrictions or access to [downloads](https://developers.cloudflare.com/stream/viewing-videos/download-videos/). This token can be customized by providing additional properties in the request:

JavaScript

```

  const signed_url_restrictions = {

    // Extend the lifetime of the token to 12 hours:

    exp: Math.floor(Date.now() / 1000) + 12 * 60 * 60,

    // Allow access to MP4 or Audio Download URLs:

    downloadable: true,

    // Geo or IP access restrictions:

    accessRules: {

      // ... see examples below

    }

  };


  const init = {

    method: "POST",

    headers: {

      Authorization: "Bearer <API_TOKEN>",

      "content-type": "application/json;charset=UTF-8",

    },

    body: JSON.stringify(signed_url_restrictions),

  };


  const signedurl_service_response = await fetch(

    "https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/{video_uid}/token",

    init,

  );


  return new Response(

    JSON.stringify(await signedurl_service_response.json()),

    { status: 200 },

  );


```

However, if you are generating tokens programmatically or adding customizations like these, it is faster and more scalable to use a signing key and generate the token within your application entirely.

## Option 2: Using the Stream binding

If you are using the Stream binding in your Worker, you can generate signed tokens without making a separate API call to the `/token` endpoint or managing signing keys yourself. The binding handles token generation internally.

Refer to [Bind to Workers API](https://developers.cloudflare.com/stream/manage-video-library/bindings/) for setup instructions.

* [  JavaScript ](#tab-panel-8487)
* [  TypeScript ](#tab-panel-8488)

JavaScript

```

export default {

  async fetch(request, env) {

    const token = await env.STREAM.video("VIDEO_ID").generateToken();

    return Response.json({ token });

  },

};


```

TypeScript

```

export default {

  async fetch(request, env) {

    const token = await env.STREAM.video("VIDEO_ID").generateToken();

    return Response.json({ token });

  },

};


```

The token generated by the binding expires in one hour by default. If you need to customize restrictions such as expiration time, geolocation, or download access, use [a signing key](#option-3-using-a-signing-key-to-create-signed-tokens) to create tokens with custom claims.

## Option 3: Using a signing key to create signed tokens

If you are generating a high-volume of tokens, using [Live WebRTC](https://developers.cloudflare.com/stream/webrtc-beta/), or need to customize the access rules, generate new tokens using a signing key so you do not need to call the Stream API each time.

### Step 1: Call the `/stream/key` endpoint _once_ to obtain a key

Terminal window

```

curl --request POST \

"https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/keys" \

--header "Authorization: Bearer <API_TOKEN>"


```

The response will return `pem` and `jwk` values.

```

{

  "result": {

    "id": "8f926b2b01f383510025a78a4dcbf6a",

    "pem": "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBemtHbXhCekFGMnBIMURiWmgyVGoyS3ZudlBVTkZmUWtNeXNCbzJlZzVqemRKTmRhCmtwMEphUHhoNkZxOTYveTBVd0lBNjdYeFdHb3kxcW1CRGhpdTVqekdtYW13NVgrYkR3TEdTVldGMEx3QnloMDYKN01Rb0xySHA3MDEycXBVNCtLODUyT1hMRVVlWVBrOHYzRlpTQ2VnMVdLRW5URC9oSmhVUTFsTmNKTWN3MXZUbQpHa2o0empBUTRBSFAvdHFERHFaZ3lMc1Vma2NsRDY3SVRkZktVZGtFU3lvVDVTcnFibHNFelBYcm9qaFlLWGk3CjFjak1yVDlFS0JCenhZSVEyOVRaZitnZU5ya0t4a2xMZTJzTUFML0VWZkFjdGkrc2ZqMkkyeEZKZmQ4aklmL2UKdHBCSVJZVDEza2FLdHUyYmk0R2IrV1BLK0toQjdTNnFGODlmTHdJREFRQUJBb0lCQUYzeXFuNytwNEtpM3ZmcgpTZmN4ZmRVV0xGYTEraEZyWk1mSHlaWEFJSnB1MDc0eHQ2ZzdqbXM3Tm0rTFVhSDV0N3R0bUxURTZacy91RXR0CjV3SmdQTjVUaFpTOXBmMUxPL3BBNWNmR2hFN1pMQ2wvV2ZVNXZpSFMyVDh1dGlRcUYwcXpLZkxCYk5kQW1MaWQKQWl4blJ6UUxDSzJIcmlvOW1KVHJtSUUvZENPdG80RUhYdHpZWjByOVordHRxMkZrd3pzZUdaK0tvd09JaWtvTgp2NWFOMVpmRGhEVG0wdG1Vd0tLbjBWcmZqalhRdFdjbFYxTWdRejhwM2xScWhISmJSK29PL1NMSXZqUE16dGxOCm5GV1ZEdTRmRHZsSjMyazJzSllNL2tRVUltT3V5alY3RTBBcm5vR2lBREdGZXFxK1UwajluNUFpNTJ6aTBmNloKdFdvwdju39xOFJWQkwxL2tvWFVmYk00S04ydVFadUdjaUdGNjlCRDJ1S3o1eGdvTwowVTBZNmlFNG9Cek5GUW5hWS9kayt5U1dsQWp2MkgraFBrTGpvZlRGSGlNTmUycUVNaUFaeTZ5cmRkSDY4VjdIClRNRllUQlZQaHIxT0dxZlRmc00vRktmZVhWY1FvMTI1RjBJQm5iWjNSYzRua1pNS0hzczUyWE1DZ1lFQTFQRVkKbGIybDU4blVianRZOFl6Uk1vQVo5aHJXMlhwM3JaZjE0Q0VUQ1dsVXFZdCtRN0NyN3dMQUVjbjdrbFk1RGF3QgpuTXJsZXl3S0crTUEvU0hlN3dQQkpNeDlVUGV4Q3YyRW8xT1loMTk3SGQzSk9zUythWWljemJsYmJqU0RqWXVjCkdSNzIrb1FlMzJjTXhjczJNRlBWcHVibjhjalBQbnZKd0k5aUpGVUNnWUVBMjM3UmNKSEdCTjVFM2FXLzd3ekcKbVBuUm1JSUczeW9UU0U3OFBtbHo2bXE5eTVvcSs5aFpaNE1Fdy9RbWFPMDF5U0xRdEY4QmY2TFN2RFh4QWtkdwpWMm5ra0svWWNhWDd3RHo0eWxwS0cxWTg3TzIwWWtkUXlxdjMybG1lN1JuVDhwcVBDQTRUWDloOWFVaXh6THNoCkplcGkvZFhRWFBWeFoxYXV4YldGL3VzQ2dZRUFxWnhVVWNsYVlYS2dzeUN3YXM0WVAxcEwwM3h6VDR5OTBOYXUKY05USFhnSzQvY2J2VHFsbGVaNCtNSzBxcGRmcDM5cjIrZFdlemVvNUx4YzBUV3Z5TDMxVkZhT1AyYk5CSUpqbwpVbE9ldFkwMitvWVM1NjJZWVdVQVNOandXNnFXY21NV2RlZjFIM3VuUDVqTVVxdlhRTTAxNjVnV2ZiN09YRjJyClNLYXNySFVDZ1lCYmRvL1orN1M3dEZSaDZlamJib2h3WGNDRVd4eXhXT2ZMcHdXNXdXT3dlWWZwWTh4cm5pNzQKdGRObHRoRXM4SHhTaTJudEh3TklLSEVlYmJ4eUh1UG5pQjhaWHBwNEJRNTYxczhjR1Z1ZSszbmVFUzBOTDcxZApQL1ZxUWpySFJrd3V5ckRFV2VCeEhUL0FvVEtEeSt3OTQ2SFM5V1dPTGJvbXQrd3g0NytNdWc9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=",

    "jwk": "eyJ1c2UiOiJzaWciLCJrdHkiOiJSU0EiLCJraWQiOiI4ZjkyNmIyYjAxZjM4MzUxNzAwMjVhNzhhNGRjYmY2YSIsImFsZyI6IlJTMjU2IiwibiI6InprR214QnpBRjJwSDFEYlpoMlRqMkt2bnZQVU5GZlFrTXlzQm8yZWc1anpkSk5kYWtwMEphUHhoNkZxOTZfeTBVd0lBNjdYeFdHb3kxcW1CRGhpdTVqekdtYW13NVgtYkR3TEdTVldGMEx3QnloMDY3TVFvTHJIcDcwMTJxcFU0LUs4NTJPWExFVWVZUGs4djNGWlNDZWcxV0tFblREX2hKaFVRMWxOY0pNY3cxdlRtR2tqNHpqQVE0QUhQX3RxRERxWmd5THNVZmtjbEQ2N0lUZGZLVWRrRVN5b1Q1U3JxYmxzRXpQWHJvamhZS1hpNzFjak1yVDlFS0JCenhZSVEyOVRaZi1nZU5ya0t4a2xMZTJzTUFMX0VWZkFjdGktc2ZqMkkyeEZKZmQ4aklmX2V0cEJJUllUMTNrYUt0dTJiaTRHYi1XUEstS2hCN1M2cUY4OWZMdyIsImUiOiJBUUFCIiwiZCI6IlhmS3FmdjZuZ3FMZTktdEo5ekY5MVJZc1ZyWDZFV3RreDhmSmxjQWdtbTdUdmpHM3FEdU9henMyYjR0Um9mbTN1MjJZdE1UcG16LTRTMjNuQW1BODNsT0ZsTDJsX1VzNy1rRGx4OGFFVHRrc0tYOVo5VG0tSWRMWlB5NjJKQ29YU3JNcDhzRnMxMENZdUowQ0xHZEhOQXNJcllldUtqMllsT3VZZ1Q5MEk2MmpnUWRlM05oblN2MW42MjJyWVdURE94NFpuNHFqQTRpS1NnMl9sbzNWbDhPRU5PYlMyWlRBb3FmUld0LU9OZEMxWnlWWFV5QkRQeW5lVkdxRWNsdEg2Zzc5SXNpLU04ek8yVTJjVlpVTzdoOE8tVW5mYVRhd2xnei1SQlFpWTY3S05Yc1RRQ3VlZ2FJQU1ZVjZxcjVUU1Ai2odx5iT0xSX3BtMWFpdktyUSIsInAiOiI5X1o5ZUpGTWI5X3E4UlZCTDFfa29YVWZiTTRLTjJ1UVp1R2NpR0Y2OUJEMnVLejV4Z29PMFUwWTZpRTRvQnpORlFuYVlfZGsteVNXbEFqdjJILWhQa0xqb2ZURkhpTU5lMnFFTWlBWnk2eXJkZEg2OFY3SFRNRllUQlZQaHIxT0dxZlRmc01fRktmZVhWY1FvMTI1RjBJQm5iWjNSYzRua1pNS0hzczUyWE0iLCJxIjoiMVBFWWxiMmw1OG5VYmp0WThZelJNb0FaOWhyVzJYcDNyWmYxNENFVENXbFVxWXQtUTdDcjd3TEFFY243a2xZNURhd0JuTXJsZXl3S0ctTUFfU0hlN3dQQkpNeDlVUGV4Q3YyRW8xT1loMTk3SGQzSk9zUy1hWWljemJsYmJqU0RqWXVjR1I3Mi1vUWUzMmNNeGNzMk1GUFZwdWJuOGNqUFBudkp3STlpSkZVIiwiZHAiOiIyMzdSY0pIR0JONUUzYVdfN3d6R21QblJtSUlHM3lvVFNFNzhQbWx6Nm1xOXk1b3EtOWhaWjRNRXdfUW1hTzAxeVNMUXRGOEJmNkxTdkRYeEFrZHdWMm5ra0tfWWNhWDd3RHo0eWxwS0cxWTg3TzIwWWtkUXlxdjMybG1lN1JuVDhwcVBDQTRUWDloOWFVaXh6THNoSmVwaV9kWFFYUFZ4WjFhdXhiV0ZfdXMiLCJkcSI6InFaeFVVY2xhWVhLZ3N5Q3dhczRZUDFwTDAzeHpUNHk5ME5hdWNOVEhYZ0s0X2NidlRxbGxlWjQtTUswcXBkZnAzOXIyLWRXZXplbzVMeGMwVFd2eUwzMVZGYU9QMmJOQklKam9VbE9ldFkwMi1vWVM1NjJZWVdVQVNOandXNnFXY21NV2RlZjFIM3VuUDVqTVVxdlhRTTAxNjVnV2ZiN09YRjJyU0thc3JIVSIsInFpIjoiVzNhUDJmdTB1N1JVWWVubzIyNkljRjNBaEZzY3NWam55NmNGdWNGanNIbUg2V1BNYTU0dS1MWFRaYllSTFBCOFVvdHA3UjhEU0NoeEhtMjhjaDdqNTRnZkdWNmFlQVVPZXRiUEhCbGJudnQ1M2hFdERTLTlYVF8xYWtJNngwWk1Mc3F3eEZuZ2NSMF93S0V5Zzh2c1BlT2gwdlZsamkyNkpyZnNNZU9fakxvIn0=",

    "created": "2021-06-15T21:06:54.763937286Z"

  },

  "success": true,

  "errors": [],

  "messages": []

}


```

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](https://developers.cloudflare.com/workers/configuration/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:

JavaScript

```

// 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](https://developers.cloudflare.com/stream/viewing-videos/download-videos/), 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.

Terminal window

```

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 are `any`, `ip.src` and `ip.geoip.country`
* `action`: support values are `allow` and `block`

Depending on the rule type, accessRules support 2 additional properties:

* `country`: an array of 2-letter country codes in [ISO 3166-1 Alpha 2 ↗](https://www.iso.org/obp/ui/#search) 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.com` covers `a.badtortilla.com`, `a.b.badtortilla.com` and does not cover `badtortilla.com`
* `example.com` does not cover [www.example.com ↗](http://www.example.com) or any subdomain of example.com
* `localhost` requires 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.com` covers `example.com/\*`

You can also control embed limitation programmatically using the Stream API. `uid` in the example below refers to the video id.

Terminal window

```

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:

* [  JavaScript ](#tab-panel-8489)
* [  TypeScript ](#tab-panel-8490)

JavaScript

```

export default {

  async fetch(request, env) {

    const video = await env.STREAM.video("VIDEO_ID").update({

      allowedOrigins: ["example.com"],

    });

    return Response.json(video);

  },

};


```

TypeScript

```

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.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/stream/","name":"Stream"}},{"@type":"ListItem","position":3,"item":{"@id":"/stream/viewing-videos/","name":"Play video"}},{"@type":"ListItem","position":4,"item":{"@id":"/stream/viewing-videos/securing-your-stream/","name":"Secure your Stream"}}]}
```
