> ## Documentation Index
> Fetch the complete documentation index at: https://cloudinary.com/documentation/llms.txt
> Use this file to discover all available pages before exploring further.

# Media access methods


By default, Cloudinary assets are accessible via a public CDN once uploaded using the `upload` delivery type. While you can use randomly generated public IDs to make URLs harder to guess, this alone doesn't prevent access.

For greater control, Cloudinary offers multiple access protection features, from simple delivery restrictions to advanced, token-based authentication.

## Access protection options

### Protect delivery and transformations

These options prevent unauthorized transformations and may also restrict access to original or derived assets.

{table:class=set-first-col} **Feature** | **What it Does** | **Best Used For** |
|-------------|------------------|--------------------|
| **Transformation Control** |||
| [Strict transformations](#strict_transformations) | Blocks on-the-fly transformations unless explicitly allowed (via [eager](eager_and_incoming_transformations#eager_transformations), [signed](#enforcement_mechanism_signed_delivery_urls), or [referral-domain sources](#allowed_strict_referral_domains)). | Preventing unauthorized manipulation of image and video transformations (e.g., removing overlays, bypassing cropping). |
| **Restrictive Delivery Types**[Set explicitly on upload](upload_parameters#delivery_types) |||
| [Private](#private_media_assets) 1 | Requires a signed URL to access the original asset, while derived versions remain public. Use with **strict transformations** to prevent unauthorized creation of new derived assets. | Protecting original files while allowing public access variations (e.g., resized or watermarked). |
| [Authenticated](#authenticated_media_assets) 1 | Requires a signed URL to access both original and derived assets. Prevents users from guessing signed URLs or modifying their transformations. | Fully restricts access to assets and transformations. |
| **Enforcement Mechanism** |||
| [Signed URLs](#enforcement_mechanism_signed_delivery_urls) 2 | Authorizes access to `private` and `authenticated` assets, and dynamic transformations when using **strict transformations**. **Caution:** Doesn't prevent link sharing. Once a URL is exposed (e.g., on your website), anyone with it can access the asset.| Validating URL authenticity and restricting unauthorized access.  |

{notes:title=Footnotes}

1.   To prevent sensitive private and authenticated assets from appearing in search engine results, use the `explicit` endpoint's optional [header](image_upload_api_reference#explicit_optional_parameters) parameter to include `X-Robots-Tag: noindex` when uploading or configuring the asset's response headers.
2.  You'll also need to sign delivery URLs when using transformation add-ons that require it (e.g., [OCR Text Extraction](ocr_text_detection_and_extraction_addon) and [Cloudinary AI Content Analysis](cloudinary_ai_content_analysis_addon)), or when delivering assets using the [fetch](fetch_remote_images#fetch_and_deliver_remote_files) delivery type if **Fetched URL** is selected as a **Restricted media type** in your product environment settings.
{/note}

### Prevent search engine indexing

If you want to exclude assets from search engine results:

* **On upload**: Pass response headers using the Upload API headers parameter, for example:

    ```curl 
    curl -X POST "https://api.cloudinary.com/v1_1/<cloud_name>/image/upload" \
      -F "file=@/path/to/file.jpg" \
      -F "api_key=<api_key>" \
      -F "timestamp=$(date +%s)" \
      -F "signature=<signature>" \
      -F 'headers=X-Robots-Tag: noindex'
    ```

    See [Upload API](image_upload_api_reference#upload) for more details.

* **For existing assets**: Use the [explicit method](image_upload_api_reference#explicit) to (re)configure response headers without re-uploading (e.g., add `X‑Robots‑Tag: noindex`).

* **Via upload presets**: You can add the same headers setting to an [upload preset](upload_presets) and apply it consistently wherever that preset is used (API, Upload Widget, or Console UI).

> **NOTE**: Excluding assets from indexing doesn't change access permissions. To fully restrict who can view assets, combine this with the access‑control options on this page (e.g., private delivery types, authenticated delivery types, and signed URLs).

### Control who can access assets and when

These options prevent access to existing files and control who can view them:

{table:class=set-first-col} **Feature** | **What it Does** | **Best Used For** |
|-------------|------------------|--------------------|
| **Access Control** |||
| [Access-controlled assets](#access_controlled_media_assets) | Requires a valid token or cookie to access. (Allows you to define time-limited public access.) | Gaining full control over who can access an asset and when. |
| [Time-limited public access](#time_limited_public_access) | Allows public access during a specific time window for otherwise restricted assets. | Temporarily exposing an access-controlled asset before restricting it. |
| [Token-based access](#token_based_access_premium_feature)Premium feature | Appends a signed token to URLs to restrict access by time, IP, or a specific URL pattern (using an Access Control List). | Delivering personalized or short-lived access (e.g., post-purchase, internal tools). |
| [Cookie-based access](#cookie_based_access_premium_feature)Premium feature + CNAME | Similar to token-based access, but uses a signed browser cookie instead of a URL token. | Enforcing session-based access (e.g., logged-in web or mobile users). |
| **Product Environment Rules** |||
| [Product environment-level ACLs](#product_environment_access_control_list_premium_feature)Premium feature | Globally allows or blocks access based on IP, country, Referer, User-Agent, and more. | Blocking unwanted traffic (e.g., bots, regions, or referrers) at the environment level. |

## Strict transformations

Cloudinary transformation URLs are dynamic by default. If a transformed version doesn't exist, it's generated on the fly. While flexible, this can lead to unintended usage (e.g., transformation spamming or bypassing overlays).

To prevent this, you can enable **Strict Transformations** from the [Security](https://console.cloudinary.com/app/settings/security) page in your Cloudinary Console. Once enabled, transformations can only be created through:

* [Eager transformations](eager_and_incoming_transformations#eager_transformations)
* [Signed delivery URL](#enforcement_mechanism_signed_delivery_urls)
* [Allowed name transformations](#allow_transformation)
* [Allowed referral domains](#allowed_strict_referral_domains)

### Define allowed transformations

Once you've enabled strict transformations for your product environment, you can allow specific transformations either using the Admin API or the UI.

#### Using the Admin API

Using the [update transformation](admin_api#update_transformation) method of the Admin API, set `allowed_for_strict` to `true` for the transformation you want to allow.

For example, allow a transformation by name (here, `on_sale_overlay` is a [named transformation](image_transformations#named_transformations)):

```multi

|ruby
result = Cloudinary::Api
.update_transformation('on_sale_overlay',
  allowed_for_strict: true)

|php_2
$result = $api
->updateTransformation("on_sale_overlay",
  ["allowed_for_strict" => 1]);

result = cloudinary.api\
.update_transformation("on_sale_overlay",
  allowed_for_strict = True)

|nodejs
cloudinary.v2.api
.update_transformation('on_sale_overlay',
  { allowed_for_strict: true })
.then(result=>console.log(result)); 

|java
result = api
.updateTransformation("on_sale_overlay",
  ObjectUtils.asMap("allowed_for_strict", true));

|csharp
var updateTransformParams = new UpdateTransformParams(){
  Transformation = "on_sale_overlay",
  Strict = true};
cloudinary.UpdateTransform(updateTransformParams);

|go
resp, err := cld.Admin.UpdateTransformation(ctx, admin.UpdateTransformationParams{
      Transformation:   "on_sale_overlay", 
      AllowedForStrict: api.Bool(true)})

|curl
curl \
  -d 'allowed_for_strict=true' \
  -X PUT \
  https://<API_KEY>:<API_SECRET>@api.cloudinary.com/v1_1/<CLOUD_NAME>/transformations/on_sale_overlay

|cli
cld admin update_transformation "on_sale_overlay" allowed_for_strict=true

```

Or, allow a transformation by parameter listing:

```multi

|ruby
result = Cloudinary::Api
.update_transformation(
  { width: 150, height: 100, crop: "fill"},
  { allowed_for_strict: true })

|php_2
$result = $api
->updateTransformation(
  ["width" => 150, "height" => 100,"crop" => "fill"],
  ["allowed_for_strict" => 1]);

|python
result = cloudinary.api\
.update_transformation(
  dict(width = 150, height = 100, crop = "fill"), 
  allowed_for_strict = True)

|nodejs
cloudinary.v2.api
.update_transformation(
  { width: 150, height: 100, crop: 'fill' },
  { allowed_for_strict: true })
.then(result=>console.log(result)); 

|java
result = api
.updateTransformation(
  new Transformation().width(150).height(100).crop("fill").generate(), 
  ObjectUtils.asMap("allowed_for_strict", true));

|csharp
var updateTransformParams = new UpdateTransformParams(){
  Transformation = "w_150,h_100,c_fill",
  Strict = true};
cloudinary.UpdateTransform(updateTransformParams);

|go
resp, err := cld.Admin.UpdateTransformation(ctx, admin.UpdateTransformationParams{
      Transformation:   "w_150,h_100,c_fill", 
      AllowedForStrict: true})

|curl
curl \
  -d 'allowed_for_strict=true' \
  -X PUT \
  https://<API_KEY>:<API_SECRET>@api.cloudinary.com/v1_1/<CLOUD_NAME>/transformations/c_fill,h_100,w_150

|cli
cld admin update_transformation '{"width": 150, "height": 100, "crop": "fill"}' allowed_for_strict=true

```
> **NOTE**: If running the CLI command on Windows, you need to escape the double quotes within the curly braces using either `\` or `"`, for example, `\"text\"` or `""text""`.
#### Using the UI

Open the **Manage Transformations** page in the **Image** product of the Console: **[Image > Manage Transformations](https://console.cloudinary.com/app/image/manage/)**. 

You can find your transformations listed in both the [Named Transformations](https://console.cloudinary.com/app/image/manage/named) or [Log](https://console.cloudinary.com/app/image/manage/log) tabs. 

For each relevant transformation, open the kebab menu and toggle the **Allowed for strict transformations** option.

![enable transformation](https://cloudinary-res.cloudinary.com/image/upload/bo_1px_solid_gray/q_auto,f_auto/docs/enable-strict-transformations-ui.png "thumb: c_scale,w_650/dpr_2, width: 650px, popup:true")

Once allowed for strict transformations, you can use the transformation on any asset to generate and deliver the transformed version of that asset on the fly. 

For transformations that aren't allowed, if you try to apply the transformation to an asset you'll see a 404 error.

 
#### Unique transformations

Some transformations deliver the same result, but aren't seen as identical by Cloudinary. Therefore, ensure you allow each variation of a transformation as required. Here are some examples where you may think the transformation is the same, but Cloudinary sees it as a different transformation:
* If the order of the transformation parameters changes. For example, `w_300,c_scale` is considered a different transformation to `c_scale,w_300`, even though the delivered media looks the same.
* If the file extension is different. For example, `.jpg` is considered a different transformation than `.jpeg`, or a JPEG file with no extension specified, even though a JPEG file is the delivered format in each case.
* Whether or not the file format is specified as a transformation. For example, specifying `f_jpg` as a transformation parameter for a JPEG file is considered a different transformation than delivering the same transformation with a `.jpg` extension, even though a JPEG file is delivered in each case.

> **INFO**: There are certain considerations to be aware of if enabling strict transformations after using your Cloudinary product environment for some time. For example, any derived asset generated while a particular transformation was still allowed, remains accessible until the cache is invalidated. You'll need to adapt certain transformation combinations to work with Strict Transformations enabled, so we recommend that you [contact us](https://support.cloudinary.com/hc/en-us/requests/new) to help identify any potential issues with the transition.

#### Using automatic format in strict transformations

You can specify [f_auto (automatic format)](transformation_reference#f_auto) in a transformation that you want to allow when you have strict transformations enabled because Cloudinary allows all format variations for the transformation in this case.

You can't use `f_auto` directly within a [named transformation](image_transformations#named_transformations), so you need to combine `f_auto` and your named transformation, and then allow that transformation. 

Here's one way to do that if you already have strict transformations enabled:

1. Create a new transformation using the [Transformation Builder](https://console.cloudinary.com/app/image/transformation_builder). Save the transformation as a named transformation. In this example, it has the name `scale356`. Notice that the builder signs the delivery URL if you've enabled strict transformations.
      ![Transformation builder](https://cloudinary-res.cloudinary.com/image/upload/f_auto/q_auto/docs/tx-builder-named-tx.png "thumb: c_scale,w_600/dpr_2, width: 600px, popup:true")
1. Use the [Cloudinary CLI](cloudinary_cli#url) to create a signed URL for that named transformation together with the `f_auto` parameter. You can use any asset; in this example we use `cld-sample-5.jpg`:
      
      ```
      cld url -s cld-sample-5.jpg 't_scale356/f_auto'
      ```

1. The CLI returns a signed URL, but doesn't create the derived asset. To create the derived asset, causing the `t_scale356/f_auto` transformation to be visible in the Log, use a cURL command to request the derived asset, for example:
      
      ```
      curl https://res.cloudinary.com/my_cloud/image/upload/s--gSk8FE-8--/t_scale356/f_auto/cld-sample-5.jpg --output cld-sample-5.jpg
      ``` 

1. You'll then see the transformation `t_scale356/f_auto` present in the [Log](https://console.cloudinary.com/app/image/manage/log), which you can [allow for strict transformations](#using_the_ui).
      ![Allow f_auto with a named transformation](https://cloudinary-res.cloudinary.com/image/upload/bo_1px_solid_gray/f_auto/q_auto/docs/allow-fauto-strict-tx.png "thumb: c_scale,w_600/dpr_2, width: 600px, popup:true")

### Allowed strict referral domains

You can use the **Allowed strict referral domains** setting to set the referrer domains that you want to permit to generate unsigned transformations on the fly, even when you've got strict transformations enabled. Specify the domain(s) in the [Security](https://console.cloudinary.com/app/settings/security) page of the Cloudinary Console Settings. Separate multiple domain entries with a newline. 

> **NOTE**: Keep in mind that this option only determines which referral domains can _generate_ new unsigned transformations.  It doesn't restrict other domains from accessing a transformation URL once the transformed asset is generated (via a signed request or by an allowed domain).

## Restrictive delivery types

Delivery types control access behavior and must be set at upload using the type parameter. These delivery types do not work in isolation. They require [signed URLs](#enforcement_mechanism_signed_delivery_urls) and may integrate with asset-level [access control](#access_controlled_media_assets).

### Private media assets

When you upload images and videos as [private](upload_parameters#private_assets), the original file is protected and only accessible via a [signed URL](#enforcement_mechanism_signed_delivery_urls) However, by default, any derived (transformed) versions of the asset are publicly accessible. 

For example, the following delivery URL for the `sample` image returns an error:

`https://res.cloudinary.com/demo/image/private/sample.jpg`

But a derived version is accessible, such as the `sample` private image with a width of 300 pixels:

`https://res.cloudinary.com/demo/image/private/w_300/sample.jpg`

To deliver a derived version of a private asset, set the `type` parameter in the delivery URL to `private` (instead of the default `upload`).

To prevent public access even to derived assets, you can enable [Strict Transformations](#strict_transformations) which blocks transformation URLs unless they are explicitly allowlisted or signed.

#### Providing time-limited access to private media assets

You can make a private asset temporarily accessible (for example, to enable a customer to access a stock photo on your site after purchasing it) by generating a time-limited and signed URL with the `private_download_url` method of the Cloudinary API. 

The generated URL delivers the image via a secure authenticated API request to Cloudinary each time. The image isn't cached on the CDN. 

> **INFO**: You shouldn't embed these time-limited URLs on your website as they cost twice the bandwidth when delivering them.

**Required parameters**:

* `public_id` - The identifier of the uploaded asset.
* `format` - The asset file format. 

**Optional parameters**:

* `resource_type` - The resource type (`image`, `video` or `raw`) of the file to deliver. Default: `image`. 
* `type` - The delivery type of the file to deliver. Possible values: `upload`, `private`, `authenticated`. Default: `private`.
* `expires_at` - The date (UNIX time in seconds) for the URL expiration. Default: one hour from the time the URL is generated.
* `attachment`- If `true`, the URL downloads the image as an attachment. Otherwise, the image is displayed. Default: `false`.
* `transformation` - A transformation to apply to the delivered image or video (not currently supported by the SDKs).

#### Example 1: Image with default expiry time 

Generate a link to the `my_picID` image in JPEG format, which is valid for one hour:

```multi
|ruby
Cloudinary::Utils.private_download_url('my_picID', 'jpg')

|php_2
(new Cloudinary\Api\Admin\AdminApi())->privateDownloadUrl('my_picID', 'jpg');

|python
cloudinary.utils.private_download_url("my_picID", "jpg")

|nodejs
cloudinary.v2.utils.private_download_url('my_picID', 'jpg');

|java
cloudinary.utils().privateDownload("my_picID", "jpg", 
    ObjectUtils.emptyMap());

|csharp
cloudinary.DownloadPrivate("my_picID", format: "jpg");

|go
resp, err := cld.Upload.PrivateDownloadURL(uploader.PrivateDownloadURLParams{
		PublicID: "my_picID",
		Format:   "jpg"})

|cli
cld utils private_download_url "my_picID" "jpg"
``` 

This generates a link similar to the following:

`https://api.cloudinary.com/v1_1/private-demo/image/download?timestamp=1753601889&public_id=my_picID&format=jpg&signature=d994c2b972c30d84d33fde684aa377fc17878be6&api_key=824698761754661`

#### Example 2: Video with extended expiry time

Generate a link to the `my_video` video in MP4 format, which is valid for two hours:

```multi
|ruby
expires_time = Time.now.to_i + 7200  # 2 hours from now
Cloudinary::Utils.private_download_url('my_video', 'mp4', 
  expires_at: expires_time,
  resource_type: "video")

|php_2
$expires_time = time() + 7200;  // 2 hours from now
(new Cloudinary\Api\Admin\AdminApi())->privateDownloadUrl('my_video', 'mp4', [
  "expires_at" => $expires_time,
  "resource_type" => "video"
]);

|python
import time
expires_time = int(time.time()) + 7200  # 2 hours from now
cloudinary.utils.private_download_url("my_video", "mp4",
  expires_at=expires_time,
  resource_type="video")

|nodejs
const expires_time = Math.floor(Date.now() / 1000) + 7200;  // 2 hours from now
cloudinary.v2.utils.private_download_url('my_video', 'mp4', {
  expires_at: expires_time,
  resource_type: "video"
});

|java
long expiresTime = System.currentTimeMillis() / 1000 + 7200;  // 2 hours from now
cloudinary.utils().privateDownload("my_video", "mp4", 
    ObjectUtils.asMap(
      "expires_at", expiresTime,
      "resource_type", "video"));

|csharp
var expiresTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 7200;  // 2 hours from now
cloudinary.DownloadPrivate("my_video", 
  format: "mp4",
  expiresAt: expiresTime,
  resourceType: ResourceType.Video);

|go
expiresTime := time.Now().Unix() + 7200  // 2 hours from now
resp, err := cld.Upload.PrivateDownloadURL(uploader.PrivateDownloadURLParams{
		PublicID: "my_video",
		Format:   "mp4",
		ResourceType: "video",
		ExpiresAt: &expiresTime})

|cli
expires_time=$(date -v +2H +%s)  # 2 hours from now (macOS)
# or for Linux: expires_time=$(date -d '+2 hours' +%s)
cld utils private_download_url "my_video" "mp4" expires_at=$expires_time resource_type="video"
``` 

This generates a link similar to the following:

`https://api.cloudinary.com/v1_1/private-demo/video/download?timestamp=1753443184&public_id=my_video&format=mp4&expires_at=1753450172&signature=013b1ff88c8b7e81ca9eb93ee5b31cce86ca709d&api_key=824698761754661`

### Authenticated media assets

When you upload images and videos as [authenticated](upload_parameters#authenticated_assets), the original file and any derived (transformed) versions are protected and only accessible via a [signed URL](#enforcement_mechanism_signed_delivery_urls). 

For example, the following delivery URL for the `auth_sample` image returns an error:

`https://res.cloudinary.com/demo/image/authenticated/auth_sample.jpg`

Derived versions also return an error, such as the `auth_sample` image with a width of 300 pixels:

`https://res.cloudinary.com/demo/image/authenticated/_w_300/auth_sample.jpg`

To access the asset, you'll need to use the exact signature in the delivery URL: 

![Authenticated image](https://res.cloudinary.com/demo/image/authenticated/s--HILJKWs4--/v1753862893/auth_sample.jpg "with_image:false, with_url:true, with_code:false")

You can’t create on-the-fly transformations for authenticated assets. Instead, you can only access derived assets that were generated ahead of time during upload or update using eager transformations. Each of these includes its own signature, which you can retrieve from the upload or update response.

> **NOTE**: The `authenticated` delivery type is an element of the URL. Therefore, an asset uploaded with this type is always an authenticated asset. Changing the type also requires changing the delivery URL, which can be done using the `to_type` parameter of the [Rename](image_upload_api_reference#rename_method) method.

## Enforcement mechanism: Signed delivery URLs

A signed Cloudinary delivery URL is a delivery URL that has its signature validated before making the asset available for view (see the article about [on-the-fly image transformations secured with signed urls](/blog/on_the_fly_image_manipulations_secured_with_signed_urls) for more details). Signed delivery URLs allow you to:

* Deliver specific derived assets when [strict transformations](#strict_transformations) are enabled
* Allow access to [private and authenticated](#restrictive_delivery_types) assets

> **NOTE**:
>
> You'll also need to sign delivery URLs when using transformation add-ons that require it (e.g., [OCR Text Extraction](ocr_text_detection_and_extraction_addon) and [Cloudinary AI Content Analysis](cloudinary_ai_content_analysis_addon)), or when delivering assets using the [fetch](fetch_remote_images#fetch_and_deliver_remote_files) delivery type if **Fetched URL** is selected as a **Restricted media type** in your product environment settings.

A signed delivery URL contains a signature component of the format `/s--SIGNATURE--/`. The signature is automatically generated by Cloudinary's backend SDKs by adding the `sign_url` boolean parameter to the [helper method](image_transformations#embedding_images_in_web_pages) and setting it to true (you can manually generate a signature by taking the first eight characters of a base64 encoding of an SHA digest of a 'public_id/transformation' string concatenated with your API secret. See [Generating delivery URL signatures](delivery_url_signatures) for more information).
> **NOTE**: By default, Cloudinary supports both SHA-1 and SHA-256 digests for validation, and you can use either. The SDK methods use the SHA-1 algorithm by default, but you can use the SHA-256 algorithm instead by setting the `signature_algorithm` SDK [configuration parameter](cloudinary_sdks#configuration_parameters) to `sha256`. If you want to limit your account to allow only the SHA-256 digest for all your validations, [submit a request](https://support.cloudinary.com/hc/en-us/requests/new).

For example, to create an image tag for the authenticated image `secret_couple` resized to a height and width of 300 pixels using the fill resize mode, while signing the transformation URL using the first eight characters of an SHA signature:

![signed image tag](https://res.cloudinary.com/demo/image/authenticated/s--77PWXXNB--/c_fill,h_300,w_300/v1515589189/secret_couple.jpg "with_url: false, with_image: false")

```nodejs
cloudinary.image("secret_couple.jpg", {height: 300, width: 300, crop: "fill", sign_url: true, type: "authenticated"})
```

```react
new CloudinaryImage("secret_couple.jpg")
  .resize(fill().width(300).height(300))
  .setSignature("77PWXXNB")
  .setVersion(1515589189)
  .setDeliveryType("authenticated");
```

```vue
new CloudinaryImage("secret_couple.jpg")
  .resize(fill().width(300).height(300))
  .setSignature("77PWXXNB")
  .setVersion(1515589189)
  .setDeliveryType("authenticated");
```

```angular
new CloudinaryImage("secret_couple.jpg")
  .resize(fill().width(300).height(300))
  .setSignature("77PWXXNB")
  .setVersion(1515589189)
  .setDeliveryType("authenticated");
```

```js
new CloudinaryImage("secret_couple.jpg")
  .resize(fill().width(300).height(300))
  .setSignature("77PWXXNB")
  .setVersion(1515589189)
  .setDeliveryType("authenticated");
```

```python
CloudinaryImage("secret_couple.jpg").image(height=300, width=300, crop="fill", sign_url=True, type="authenticated")
```

```php
(new ImageTag('secret_couple.jpg'))
	->resize(Resize::fill()->width(300)
->height(300))
	->sign()
	->version(1515589189)
	->deliveryType("authenticated");
```

```java
cloudinary.url().transformation(new Transformation().height(300).width(300).crop("fill")).signed(true).type("authenticated").imageTag("secret_couple.jpg");
```

```ruby
cl_image_tag("secret_couple.jpg", height: 300, width: 300, crop: "fill", sign_url: true, type: "authenticated")
```

```csharp
cloudinary.Api.UrlImgUp.Transform(new Transformation().Height(300).Width(300).Crop("fill")).Signed(true).Action("authenticated").BuildImageTag("secret_couple.jpg")
```

```dart
cloudinary.image('secret_couple.jpg').transformation(Transformation()
	.resize(Resize.fill().width(300)
.height(300))
	.setSignature("77PWXXNB")
	.version(1515589189)
	.setDeliveryType("authenticated"));
```

```swift
imageView.cldSetImage(cloudinary.createUrl().setType( "authenticated").setTransformation(CLDTransformation().setHeight(300).setWidth(300).setCrop("fill")).generate("secret_couple.jpg", signUrl: true)!, cloudinary: cloudinary)
```

```android
MediaManager.get().url().transformation(new Transformation().height(300).width(300).crop("fill")).signed(true).type("authenticated").generate("secret_couple.jpg");
```

```flutter
cloudinary.image('secret_couple.jpg').transformation(Transformation()
	.resize(Resize.fill().width(300)
.height(300))
	.setSignature("77PWXXNB")
	.version(1515589189)
	.setDeliveryType("authenticated"));
```

```kotlin
cloudinary.image {
	publicId("secret_couple.jpg")
	 resize(Resize.fill() { width(300)
 height(300) })
	 signature()
	 version(1515589189)
	 deliveryType("authenticated") 
}.generate()
```

```jquery
$.cloudinary.image("secret_couple.jpg", {height: 300, width: 300, crop: "fill", type: "authenticated"})
```

```react_native
new CloudinaryImage("secret_couple.jpg")
  .resize(fill().width(300).height(300))
  .setSignature("77PWXXNB")
  .setVersion(1515589189)
  .setDeliveryType("authenticated");
```

And the same for a video tag, using the same transformations:

![signed video tag](https://res.cloudinary.com/demo/video/authenticated/s--77PWXXNB--/c_fill,h_300,w_300/dog.mp4 "with_url: false, with_image: false")

```nodejs
cloudinary.video("dog", {height: 300, width: 300, crop: "fill", sign_url: true, type: "authenticated"})
```

```react
new CloudinaryVideo("dog.mp4")
  .resize(fill().width(300).height(300))
  .setSignature("77PWXXNB")
  .setDeliveryType("authenticated");
```

```vue
new CloudinaryVideo("dog.mp4")
  .resize(fill().width(300).height(300))
  .setSignature("77PWXXNB")
  .setDeliveryType("authenticated");
```

```angular
new CloudinaryVideo("dog.mp4")
  .resize(fill().width(300).height(300))
  .setSignature("77PWXXNB")
  .setDeliveryType("authenticated");
```

```js
new CloudinaryVideo("dog.mp4")
  .resize(fill().width(300).height(300))
  .setSignature("77PWXXNB")
  .setDeliveryType("authenticated");
```

```python
CloudinaryVideo("dog").video(height=300, width=300, crop="fill", sign_url=True, type="authenticated")
```

```php
(new VideoTag('dog.mp4'))
	->resize(Resize::fill()->width(300)
->height(300))
	->sign()
	->deliveryType("authenticated");
```

```java
cloudinary.url().transformation(new Transformation().height(300).width(300).crop("fill")).signed(true).type("authenticated").videoTag("dog");
```

```ruby
cl_video_tag("dog", height: 300, width: 300, crop: "fill", sign_url: true, type: "authenticated")
```

```csharp
cloudinary.Api.UrlVideoUp.Transform(new Transformation().Height(300).Width(300).Crop("fill")).Signed(true).Action("authenticated").BuildVideoTag("dog")
```

```dart
cloudinary.video('dog.mp4').transformation(Transformation()
	.resize(Resize.fill().width(300)
.height(300))
	.setSignature("77PWXXNB")
	.setDeliveryType("authenticated"));
```

```swift
cloudinary.createUrl().setType( "authenticated").setResourceType("video").setTransformation(CLDTransformation().setHeight(300).setWidth(300).setCrop("fill")).generate("dog.mp4", signUrl: true)
```

```android
MediaManager.get().url().transformation(new Transformation().height(300).width(300).crop("fill")).signed(true).type("authenticated").resourceType("video").generate("dog.mp4");
```

```flutter
cloudinary.video('dog.mp4').transformation(Transformation()
	.resize(Resize.fill().width(300)
.height(300))
	.setSignature("77PWXXNB")
	.setDeliveryType("authenticated"));
```

```kotlin
cloudinary.video {
	publicId("dog.mp4")
	 resize(Resize.fill() { width(300)
 height(300) })
	 signature()
	 deliveryType("authenticated") 
}.generate()
```

```jquery
$.cloudinary.video("dog", {height: 300, width: 300, crop: "fill", type: "authenticated"})
```

```react_native
new CloudinaryVideo("dog.mp4")
  .resize(fill().width(300).height(300))
  .setSignature("77PWXXNB")
  .setDeliveryType("authenticated");
```
 
Once a derived image or video has been generated (whether dynamically on first access or [eagerly](eager_and_incoming_transformations#eager_transformations) during upload), the signature checking is skipped and the signature itself can be omitted (except for [Authenticated assets](#authenticated_media_assets), which always require either a signature or another access token).

**See also:** [Delivery URL signatures](delivery_url_signatures).

## Access-controlled media assets

**Access control** allows you to restrict the validity of your asset delivery URLs, allowing access only via:

* [Public access during a defined time window](#time_limited_public_access) (optional)
* [Token-based access](#token_based_access_premium_feature) to restrict access by time, IP address, or delivery path (ACL) *(premium feature)*  
* [Signed cookie access](#cookie_based_access_premium_feature) based on session or path pattern *(premium feature + CNAME)*

> **INFO**: Access control is available to all Cloudinary accounts. However, token-based and cookie-based access require the [Advanced plan](https://cloudinary.com/pricing) or higher. If your account doesn't support tokens, setting `access_type: token` will make the asset completely inaccessible, unless you define a public access time window.

**To activate access control:** 

* At a minimum, you must include `{ "access_type": "token" }` in the `access_control` array at [upload](image_upload_api_reference#upload) or [update](admin_api#update_details_of_an_existing_resource). This disables public access unless overridden by a matching token, signed cookie, or public time window rule.**Example:** Restrict the `sample.jpg` image to require secure delivery (via token, cookie, or public time window).

    ```multi
    |ruby  
    result = Cloudinary::Uploader.upload("sample.jpg",
      access_control: [       
        { access_type: "token" }
      ])
    
    |php_2
    $result = $cloudinary->uploadApi()->upload("sample.jpg", [ 
        "access_control" => [
          ["access_type" => "token"]
        ]]);
      
    |php
    $result = \Cloudinary\Uploader::upload("sample.jpg", [ 
        "access_control" => [
          ["access_type" => "token"]
        ]]);
      
    |python
    result = cloudinary.uploader\.upload("sample.jpg", 
      access_control = [
        {"access_type": "token"}
      ])

    |nodejs
    cloudinary.v2.uploader
    .upload("sample.jpg", 
      { access_control: [
        { access_type: "token" }
      ]}) 
      .then(result=>console.log(result));
      
    |cli
    cld uploader upload "sample.jpg" access_control='[{"access_type": "token"}]'
    ```

    > **NOTE**: If running the CLI command on Windows, you need to escape the double quotes within the curly braces using either `\` or `"`, for example, `\"text\"` or `""text""`.
> **TIP**:
>
> :title=Tips

> * You can use access control in combination with delivery type restrictions.

> * You can also view or modify [access control settings in the Media Library](media_library_for_developers#access_control).

### Time-limited public access

You can include an `anonymous` access rule to make the asset temporarily available to the public during a defined time window, no token or authentication needed. Outside this window, access is blocked unless a valid token or cookie is provided.

Use cases include:

* Post-purchase downloads

* Temporary event or campaign materials

* Embargoed content

**To set time-limited public access:**

* Combine both `token` and `anonymous` rules in the `access_control` array. The asset will be delivered if either condition is valid.**Example:** Asset requires secure access, except between Dec 15, 2022 and Jan 20, 2024, when it's publicly accessible.

    ```multi
    |ruby  
    result = Cloudinary::Uploader.upload("sample.jpg",
      access_control: [       
        { access_type: "token" },
        { access_type: "anonymous", start: "2022-12-15T12:00Z", end: "2023-01-20T12:00Z" }])
    
    |php_2
    $result = $cloudinary->uploadApi()->upload("sample.jpg", [ 
        "access_control" => [
          ["access_type" => "token"],
          ["access_type" => "anonymous", "start" => "2022-12-15T12:00Z", "end" => "2023-01-20T12:00Z" ]]]);
      
    |php
    $result = \Cloudinary\Uploader::upload("sample.jpg", [ 
        "access_control" => [
          ["access_type" => "token"],
          ["access_type" => "anonymous", "start" => "2022-12-15T12:00Z", "end" => "2023-01-20T12:00Z" ]]]);
      
    |python
    result = cloudinary.uploader\.upload("sample.jpg", 
      access_control = [
        {"access_type": "token"},
        {"access_type": "anonymous", "start": "2022-12-15T12:00Z", "end": "2023-01-20T12:00Z" }])

    |nodejs
    cloudinary.v2.uploader
    .upload("sample.jpg", 
      { access_control: [
        { access_type: "token" }, 
        { access_type: "anonymous", start: "2022-12-15T12:00Z", end: "2023-01-20T12:00Z" } ]}) 
      .then(result=>console.log(result));
      
    |cli
    cld uploader upload "sample.jpg" access_control='[{"access_type": "token"}, {"access_type": "anonymous", "start": "2022-12-15T12:00Z", "end": "2023-01-20T12:00Z" }]'
    ```

### Token-based and cookie-based access (premium features)

Token-based and cookie-based access let you flexibly and securely restrict the delivery of media assets using signed tokens. Both methods use the same token format and enforce the same access rules. Token-based access adds the token as a query string to the URL, while cookie-based access stores the token in a browser cookie.

These features are available on the [Advanced plan](https://cloudinary.com/pricing) or higher. [Contact us](https://support.cloudinary.com/hc/en-us/requests/new) to receive an encryption key provisioned for your Cloudinary product environment. You need this key to generate tokens and cookies. We also configure your environment for token validation at the CDN level.

> **INFO**: Cookie-based access additionally requires using a [Custom delivery hostname (CNAME)](advanced_url_delivery_options#private_cdns_and_custom_delivery_hostnames_cnames) at additional cost.

The following access rules apply to both token-based and cookie-based access:

* A specific time frame or expiration date

* A specific IP address

* A specific URL pattern (using an Access Control List)

To access an asset with `"access_type": "token"` (outside of any [public access window](#time_limited_public_access), if defined) the request must include a valid signed token.

> **INFO**: Before you start, make sure that you have set up your framework to access Cloudinary's APIs. For details, see the relevant [SDK framework documentation](cloudinary_sdks).

#### Generating access tokens (for token-based and cookie-based access)

For token-based (URL) access, pass the token parameters to a Cloudinary SDK method (e.g., `cl_image_tag` in Rails) using `auth_token` and `sign_url: true`, and the SDK generates and signs the token internally.

For cookie-based access, use the `generate_auth_token` method to generate the token string, which you then set as a browser cookie.

**Shared token parameters**

Both token-based (URL) and cookie-based access use the following parameters to define when and how to grant access:

* `key` – (Required) - the token must be signed with the encryption key received from Cloudinary.
* `acl` – (Optional) – an Access Control List for limiting the allowed URL path to a specified pattern (e.g., /video/authenticated/\*). The pattern can include any of Cloudinary's transformations to also apply to the delivered assets, and restrict access to a specific path, or even to a specific asset. Note that if you add an overlay (e.g., for a watermark), you should also include the *fl\_layer\_apply* flag to ensure the layer can't be modified. This parameter is useful for generating a token that can be added to a number of different URLs that share a common transformation. Without this parameter, the pattern defaults to the full URL path of the requested asset.
* `ip` – (Optional) - only this IP address can access the resource.
* `start_time` - (Optional) - timestamp of the UNIX time when the URL becomes valid. Default value: the current time.
* `duration` – (Optional)1 – the duration that the URL is valid in seconds (counted from *start\_time*).
* `expiration` - (Optional)1 - timestamp in UNIX time when the URL expires.

1 Either `duration` or `expiration` must be provided (if both are provided, `duration` is ignored).

#### Delivering token-based media assets

When using Cloudinary SDKs to create delivery URLs (e.g., `cl_image_tag` or `cl_video_tag` in Rails), you can include a token in the URL by adding:

* `sign_url: true`
* `auth_token: { ... }` with the [shared token parameters](#generating_access_tokens_for_token_based_and_cookie_based_access)

**Example 1**: To create a token-based access URL that restricts access to the `sample` authenticated image for 5 minutes and is signed with 'MyKey': 

```multi
|ruby
cl_image_tag("sample.jpg", 
  sign_url: true, 
  auth_token: { key: "MyKey", duration: 300 }, 
  type: "authenticated")
  
|php_2
ImageTag::fromParams("sample.jpg",  
  ["auth_token" => [
  	"key" => "MyKey", "duration" => 300], 
  "type" => "authenticated",  
  "sign_url" => true]); 

|python
cloudinary.CloudinaryImage("sample.jpg").image(
  type = "authenticated", 
  auth_token = dict(key = "MyKey", duration = 300),
  sign_url = True)

|nodejs
cloudinary.image("sample.jpg", { 
  type: "authenticated",  
  auth_token: {key: "MyKey", duration: 300 },
  sign_url: true })

|java
cloudinary.url().transformation(new Transformation())
  .type("authenticated")
  .authToken(new AuthToken("MyKey").duration(300))
  .signed(true)
  .imageTag("sample.jpg");

|csharp
AuthToken t = new AuthToken("MyKey").Duration(300);
string url = cloudinary.Api.UrlImgUp
  .Signed(true)
  .AuthToken(t)
  .Type("authenticated")
  .BuildUrl("sample.jpg");

|go
img, err := cld.Image("sample.jpg")
img.DeliveryType = api.Authenticated
img.Config.URL.SignURL = true
img.AuthToken.Config = &config.AuthToken{
  Key: "MyKey",
  Duration: 300}
```

**Example 2**: To create a token-based access URL that restricts access to the `dog` authenticated video until 1/1/2018 (1514764800 in UNIX time) and is signed with 'MyKey': 

```multi

|ruby
cl_video_tag("dog.mp4", 
  sign_url: true, 
  auth_token: { key: "MyKey", expiration: 1514764800 }, 
  type: "authenticated")
  
|php_2
VideoTag::fromParams("dog.mp4",  ["auth_token" => ["key" => "MyKey", "expiration" => 1514764800], 
  "type" => "authenticated", "sign_url" => true]); 

|python
cloudinary.CloudinaryVideo("dog.mp4").video(
  type = "authenticated", 
  auth_token = dict(key = "MyKey", expiration = 1514764800), 
  sign_url = True)

|nodejs
cloudinary.video("dog.mp4", { 
  type: "authenticated",  
  auth_token: {key: "MyKey", expiration: 1514764800 },
  sign_url: true })

|java
cloudinary.url().transformation(new Transformation())
  .type("authenticated")
  .authToken(new AuthToken("MyKey").expiration(1514764800))
  .signed(true))
  .videoTag("dog.mp4");

|csharp
AuthToken t = new AuthToken("MyKey").Expiration(1514764800);
string url = cloudinary.Api.Url
  .AuthToken(t)
  .Signed(true)
  .Type("authenticated")
  .BuildVideoTag("dog.mp4");

|go
dogVideo, err := cld.Video("dog.mp4")
dogVideo.DeliveryType = api.Authenticated
dogVideo.Config.URL.SignURL = true
dogVideo.Config.AuthToken = config.AuthToken{
	Key:        "MyKey",
	Expiration: 1514764800}
```

#### Creating the access cookie

To create a cookie based access token, you pass the [shared token parameters](#generating_access_tokens_for_token_based_and_cookie_based_access) to `generate_auth_token`, and set the resulting string as the value of a browser cookie named `__cld_token__`.

**Example 1**: To create a cookie that's valid for 5 minutes (300 seconds), allows access to all authenticated images, and is signed with `MY_KEY`: 

```multi

|ruby
tokenOptions  =  { 
  key: "MY_KEY", 
  duration: 300, 
  acl: "*/image/authenticated/*"
 }
 cookieToken = Cloudinary::Utils.generate_auth_token(tokenOptions)

|php_2
use Cloudinary\Asset\AuthToken;
$authToken = new AuthToken([
    'key' => 'MY_KEY',
    'duration' => 300,
    'acl' => '*/image/authenticated/*'
]);
$token = $authToken->generate();

|python
import cloudinary
import cloudinary.utils

options = {
    "duration": 300,
    "acl": "*/image/authenticated/*",
    "key":"MY_KEY"
}   

cookie_token = cloudinary.utils.generate_auth_token(**options)

|nodejs
const cloudinary = require('cloudinary').v2;

const cookie_token = cloudinary.utils.generate_auth_token({
    duration: 300,
    acl: '*/image/authenticated/*',
    key: 'MY_KEY' 
})
```

**Example 2**: For allowing access only to authenticated videos that are delivered with a specific [named transformation](image_transformations#named_transformations) (t_authorized): 

```ruby
acl: "*/video/authenticated/t_authorized/*"
```

The named transformation may add a watermark with the name of the employee, group or role who is accessing the image. In this case, a named transformation should be created via the Cloudinary Console for every user, group or role (respectively). 

**Example 3**: For allowing access only when the name of the user is applied as a text overlay on the image: 

```ruby
acl: "*/image/authenticated/l_text:Arial_45_bold:" + current_user + ",co_white/fl_no_overflow,fl_layer_apply/*"
```

So the following URL will be allowed (assuming the current user is John Smith): 

```
https://res.cloudinary.com/demo/image/authenticated/l_text:Arial_45_bold:John Smith,co_white/fl_no_overflow,fl_layer_apply/sample.jpg
```

But if a different user's name is applied, it won't match the cookie contents and will be unauthorized. 

**Example 4**: For allowing access to authenticated images and videos, where their public IDs start with 'eyes_only', only when the red, bold, 50-pixel-sized text 'Confidential' is added as a text overlay:  

```ruby
acl: ["*/image/authenticated/l_text:Arial_50_bold:Confidential,co_red/fl_layer_apply/eyes_only*", "*/video/authenticated/l_text:Arial_50_bold:Confidential,co_red/fl_layer_apply/eyes_only*"]
```

#### Cookie placement

The cookie token returned from the `generate_auth_token` method is a string that includes the cookie name (_always_ `__cld_token__`) and its value, for example:

```
__cld_token__=ip=111.222.111.222~exp=1512982559~acl=%2fimage%2fauthenticated%2f%2a~hmac=b17360091889151e9c2e2a7c713a074fdd29dc4ef1cc2fb897a0764664f3c48d
```

Set the cookie on your main domain, e.g., _app.customer.com_ or on the sub-domain that points to Cloudinary, e.g., _images.app.customer.com_.

## Product environment Access Control List (premium feature)

For each product environment, you can configure an Access Control List (ACL). This controls who can access your assets based on their country, IP address, the path segment of the request URL, content type, Referer and User-Agent.  

> **NOTE**: These options are available only for Cloudinary accounts on a [paid plan](https://cloudinary.com/pricing).

To configure the ACL, navigate to the **Delivery** page of the Console Settings and set the **ACL Settings**.

![ACL settings](https://cloudinary-res.cloudinary.com/image/upload/bo_1px_solid_gray/f_auto/q_auto/docs/acl_settings.png "thumb:c_scale,w_300,dpr_2.0, width:300, popup:true")

* Use `*` to match any number of characters at the start, middle or end of a string.
* Use `|` to separate optional matches (OR operand).
* Use `!` to specify exceptions (NOT operand).
* Use `NULL` for testing non-existent Referer and User-Agent strings.   

The conditions are tested in the following order:

1. If any of the ALLOW conditions match, the request is allowed and none of the DENY conditions are considered. 
1. If none of the ALLOW conditions match, the DENY conditions are considered.  
1. If any of the DENY conditions match, the request is denied, otherwise it’s allowed.

You can test out the logic by setting values in the **ACL Tester** tab.

Examples...

1. Allow VPN IP:
     * **Allow by IP**: 204.187.88.10|8.8.8.8 
     * **Deny by IP**: *

1. Allow VPN IP or valid Referers:
     * **Allow by IP**: 204.187.88.10|8.8.8.8
     * **Allow by Referer**: shoesByColin.com\*|\*colin.shoes\*
     * **Deny by IP**: *

1. Block hotlinking Referers:
     * **Deny by Referer**: shoesByMiki.com\*|NULL

1. Allow Referers:
     * **Allow by Referer**: shoesByColin.com\*|\*colin.shoes\* 
     * **Deny by Referer**: *

1. Block scripting User-Agents:
     * **Deny by User-Agent**: curl\*|wget\*|NULL

Learn more about the ACL settings...

* Protocols, such as `https://` and `http://`, are not considered in the Referer URL matches.
* Referer and User-Agent strings are URL-decoded before they are tested. If the match contains a space character, or other non-alphanumeric characters, it should be URL-encoded (e.g. `%20` for a space). 
* Double quotes are automatically stripped. For example, a request with `Referer: "example.com"` would be denied if **Deny by Referer** was set to `example.com|google.com`.  For this reason, to match `Referer: ""` you need to specify `NULL`.
* All comparisons are case-insensitive.
* IP address matches only support glob matches and do not support CIDR matches. Therefore, you can use IPv4 masks of /32, /24, /16, or /8  for example, `204.187.88.10`, `204.187.88.*`, `204.187.*`, or `204.*`  respectively. Multiple IP addresses should be separated by a pipe `|` symbol like this: `204.187.88.10|104.287.17.12`.
* For IPv6, masks of /128, /112, /96, /80, /64, /48, /32, /16 are supported. You must use the full octet expanded form, rather than IPv6 abbreviated syntax. 
* IP matches use the TCP Socket connecting IP address. Users of VPN or TOR or other proxy services are opaque to the match. Cellular users who are roaming will likely also report the home MNO (Mobile Network Operator) and not the current roaming country. This is by design with LTE and will likely persist even with 5G.
* Likewise, country code matches, which are based on IP Addresses are also subject to the above variances and are not 100% reliable.
* Country code matches use the two letter [ISO 3166-1](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) country code such as `US`, `CA` and `IL`. Three letter codes, such as `USA` will not match.
* The `!` operator can be used for nuanced nested matches. For example, to allow all countries except CA and then deny those in CA using a Safari browser, set **Allow by Country Code** to `!CA` and **Deny by User-Agent** to `Safari`.

> **TIP**: Regardless of these settings, access to assets is still subject to the access control features described on this page.
