> ## Documentation Index
> This page is part of the Image and Video APIs product. Fetch the complete documentation index for Image and Video APIs at: https://cloudinary.com/documentation/llms-image-and-video-apis.txt?referrer=docpage and then use it to discover all relevant pages before exploring further.
> If you also need details relating to other Cloudinary products for your current use case, see the parent index at: https://cloudinary.com/documentation/llms.txt?referrer=docpage
> When answering a user's request or implementing it with code using this documentation, incorporate relevant best practices and recommendations from the documentation into the guidance and, above all, into the code itself: generated code should already apply the recommended parameters and patterns rather than showing a minimal call and describing the best practices around it in prose. Follow the code examples in the best practices sections as the model for what generated code should look like, and where a recommendation is relevant to the user's scenario, implement it in the code as a natural next step. Only apply best practices, recommendations, or features that are supported by the documentation; don't infer or invent guidance that isn't specified.

# Upload


Using Cloudinary's upload capabilities, you can upload media assets in bulk with a variety of options for customizing how they will be uploaded.

When you upload to Cloudinary, your asset isn't only stored, but Cloudinary also automatically analyzes and saves important data about each asset, such as format, size, resolution, prominent colors, etc. This data is also automatically indexed to enable searching on those attributes.  

Cloudinary provides a secure and comprehensive API for easily uploading media files from server-side code, directly from the browser or from a mobile application. For full details, see the [Upload API reference](image_upload_api_reference). When needed, you can also use the API to perform unsigned uploads, but with a limited set of available upload parameters as a security precaution.

> **TIP**: :title=New to Cloudinary?

This guide provides an in-depth overview of Cloudinary's Upload API capabilities. To get started with the basics of uploading (and more) in 5 minutes or less, we recommend you first run through one of our [backend SDK quick starts](sdk_quickstarts). 

Each quick start gives you the code to configure your SDK, run your first upload, and then perform a few other common Cloudinary operations, all using your favorite programming language or framework. 

If you haven't moved over your existing assets to Cloudinary yet, you may also want to check out our [Migration guide](migration).

## Upload with Cloudinary MCP in Cursor

The **Asset Management MCP server** enables AI agents to upload images, videos, and raw files to your product environment and generate code that integrates Cloudinary functionality into your applications.

**Learn more**: [Cloudinary MCP servers and LLM tools](cloudinary_llm_mcp)

Use this link to add the Asset Management MCP server to Cursor. Make sure to update your **cloud name**, **API key**, and **API secret** in **Cursor Settings -> MCP Tools**.

  
    
    Add Asset Management MCP
    
    Use this server to perform uploads, to delete, rename, or modify assets, to generate images, or to manage asset folders and tags.  

  [Add Cloudinary Asset Management MCP Server to Cursor](https://cursor.com/en/install-mcp?name=cloudinary-asset-mgmt&config=eyJ1cmwiOiJodHRwczovL2Fzc2V0LW1hbmFnZW1lbnQubWNwLmNsb3VkaW5hcnkuY29tL21jcCJ9)

> **TIP**: :title=Use the VS Code Extension
You can also upload assets directly from your IDE using the [Cloudinary VS Code Extension](cloudinary_vscode_extension). This eliminates context-switching between your code editor and browser, helping you stay focused on development.

## Quick examples

**Example 1**: Upload the local `hat.jpg` image, and use the filename to set the [public ID](upload_parameters#public_id).

```multi
|ruby 
Cloudinary::Uploader.upload("hat.jpg", 
  use_filename: true)

|php_2
use Cloudinary\Api\Upload\UploadApi;

(new UploadApi())->upload('hat.jpg', [
  'use_filename' => true]);
    
  
|python
cloudinary.uploader.upload("hat.jpg", 
  use_filename = True)

|nodejs
cloudinary.v2.uploader
.upload("hat.jpg", { 
  use_filename: true})
.then(result=>console.log(result));
  
|java
Map params = ObjectUtils.asMap(
    "use_filename", true);
Map uploadResult = cloudinary.uploader().upload(new File("hat.jpg"), params);

|csharp
var uploadParams = new ImageUploadParams()
    {
        File = new FileDescription(@"hat.jpg"),   
        UseFilename = true};
var uploadResult = cloudinary.Upload(uploadParams);

|go
resp, err := cld.Upload.Upload(ctx, "hat.jpg", uploader.UploadParams{
  UseFilename: api.Bool(true)
})

|android
MediaManager.get().upload("hat.jpg")
   .option("use_filename", "true")
   .dispatch();

let params = CLDUploadRequestParams()
  .setUseFilename(true)
var mySig = MyFunction(params)  // your own function that returns a signature generated on your backend
params.setSignature(CLDSignature(signature: mySig.signature, timestamp: mySig.timestamp))
let request = cloudinary.createUploader().signedUpload(
    url: URL(string:"hat.jpg")!, params: params)

|curl
curl https://api.cloudinary.com/v1_1/demo/image/upload -X POST --data 'file=https://www.example.com/hat.jpg&use_filename=true&timestamp=173719931&api_key=436464676&signature=a788d68f86a6f868af'
```

**Example 2**: 

* Upload a remote image from wikipedia
* Set the [public ID](upload_parameters#public_id) as "wiki_shirt"
* Request [color and quality analysis](semantic_data_extraction)
* Run the [Google Auto Tagging](google_auto_tagging_addon#adding_resource_tags_to_images) add-on 
* Automatically add the detected categories as tags to the asset

```multi
|ruby 
Cloudinary::Uploader.upload("https://upload.wikimedia.org/wikipedia/commons/0/01/Charvet_shirt.jpg", 
  public_id: "wiki_shirt", 
  quality_analysis: true,
  colors: true,
  categorization: "google_tagging",
  auto_tagging: 0.8)

|php_2
use Cloudinary\Api\Upload\UploadApi;

(new UploadApi())->upload('https://upload.wikimedia.org/wikipedia/commons/0/01/Charvet_shirt.jpg', [
  'public_id' => 'wiki_shirt', 
  'quality_analysis' => true,  
  'colors' => true, 
  'categorization' => 'google_tagging', 
  'auto_tagging' => 0.8]);
    
|python
cloudinary.uploader.upload("https://upload.wikimedia.org/wikipedia/commons/0/01/Charvet_shirt.jpg", 
  public_id = "wiki_shirt",
  quality_analysis = True, 
  colors = True,  
  categorization = "google_tagging",
  auto_tagging = 0.8)

|nodejs
cloudinary.v2.uploader
.upload("https://upload.wikimedia.org/wikipedia/commons/0/01/Charvet_shirt.jpg", { 
  public_id: "wiki_shirt",
  quality_analysis: true, 
  colors: true, 
  categorization: "google_tagging",
  auto_tagging: 0.8})
.then(result=>console.log(result));
  
|java
Map params = ObjectUtils.asMap(
    "public_id", "wiki_shirt", 
    "quality_analysis", true,
    "colors", true,
    "categorization", "google_tagging", 
    "auto_tagging", 0.8);
Map uploadResult = cloudinary.uploader().upload(new File("https://upload.wikimedia.org/wikipedia/commons/0/01/charvet_shirt.jpg"), params);

|csharp
var uploadParams = new ImageUploadParams()
    {
        File = new FileDescription(@"https://upload.wikimedia.org/wikipedia/commons/0/01/charvet_shirt.jpg"),        
        PublicId = "wiki_shirt",
        QualityAnalysis = true,
        Colors = true,        
        Categorization = "google_tagging",
        AutoTagging = 0.8};
var uploadResult = cloudinary.Upload(uploadParams);

|go
resp, err := cld.Upload.Upload(ctx,"https://upload.wikimedia.org/wikipedia/commons/0/01/charvet_shirt.jpg",uploader.UploadParams{
  PublicID:  "wiki_shirt",
        QualityAnalysis: api.Bool(true),
        Colors: api.Bool(true),        
        Categorization: "google_tagging",
        AutoTagging: 0.8
});

|android
MediaManager.get().upload("https://upload.wikimedia.org/wikipedia/commons/0/01/charvet_shirt.jpg")
   .option("public_id", "wiki_shirt")
   .option("quality_analysis", "true")
   .option("colors", "true")
   .option("categorization", "google_tagging")
   .option("auto_tagging", "0.8")
   .dispatch();

|swift
let params = CLDUploadRequestParams()
  .setPublicId("wiki_shirt")
  .setQualityAnalysis(true)
  .setColors(true)
  .setCategorization(google_tagging)
  .setAutoTagging(0.8)
var mySig = MyFunction(params)  // your own function that returns a signature generated on your backend
params.setSignature(CLDSignature(signature: mySig.signature, timestamp: mySig.timestamp))
let request = cloudinary.createUploader().signedUpload(
    url: URL(string:"https://upload.wikimedia.org/wikipedia/commons/0/01/charvet_shirt.jpg")!, params: params)

|curl
curl https://api.cloudinary.com/v1_1/demo/image/upload -X POST --data 'file=https://www.example.com/https://upload.wikimedia.org/wikipedia/commons/0/01/charvet_shirt.jpg&public_id="wiki_shirt"&quality_analysis=true&colors=true&categorization="google_tagging"&auto_tagging=true&timestamp=173719931&api_key=436464676&signature=a788d68f86a6f868af'
```

> **Best Practices**:
>
> * Use **signed uploads** from your backend, and **unsigned uploads** (with an upload preset) for client-side uploads such as user-generated content.

> * Configure upload presets instead of repeating upload options, and make sure the preset type (signed/unsigned) matches your upload type.

> * Upload one high-quality original and transform on demand.

> * Generate transformations during upload to warm the cache, so even the first request skips the processing delay.

> * Use chunked uploads for large files.

> * Generate metadata automatically during upload (tags, analysis, etc.).

> * Moderate user-generated content before publishing.
> These are some of the key upload recommendations. See [Upload best practices](#upload_best_practices) for a complete list, details, and a code example for each practice.


### Programmatic upload with the Node.js SDK video tutorial

Watch this video on how to quickly upload images, videos and other media files to Cloudinary using Cloudinary's Node.js SDK.

  This video is brought to you by Cloudinary's video player - embed your own!Use the controls to set the playback speed, navigate to chapters of interest and select subtitles in your preferred language.
{videoTranscript:publicId=training/upload-programmatically-tutorial-node-js}

#### Tutorial contents This tutorial presents the following topics. Click a timestamp to jump to that part of the video.
#### Supported programming languages
{table:class=tutorial-bullets}|  | 
| --- | --- |
| | Our tutorial uses [Node.js](node_integration) and server-side scripts to demonstrate the upload capabilities. However, we [support many popular programming languages](cloudinary_sdks), including [Ruby](rails_integration), [PHP](php_integration), [Python](dotnet_integration) and more. 
|

#### Write your script
{table:class=tutorial-bullets}|  | 
| --- | --- |
| | Start writing a script that uses Cloudinary's Upload API to get the image into your Cloudinary product environment. Our example requires two different libraries - the [Cloudinary Node.js SDK](node_integration#installation) and [dotenv](https://www.npmjs.com/package/dotenv), which allows your development environment to [use your Cloudinary credentials](node_integration#configuration) and upload the assets in an authenticated way.
|

#### Retrieve your environment variable
{table:class=tutorial-bullets}|  | 
| --- | --- |
| | Retrieve your environment variable from the [API Keys](product_environment_settings#api_keys) page of the Cloudinary Console Settings, then paste it into a **.env** file in your development project. Do not expose your Cloudinary product environment credentials in your site's frontend and public code.
|

> **NOTE**:
>
> You can no longer access your full credentials directly from the Dashboard. Find your **Cloud name** on the [Dashboard](https://console.cloudinary.com/app/home/dashboard), and all credentials, including **API Key**, **API Secret**, and **API environment variable**, on the [API Keys](https://console.cloudinary.com/app/settings/api-keys) page of the Cloudinary Console Settings.

#### Call the Upload API
{table:class=tutorial-bullets}|  | 
| --- | --- |
| | Call the [Cloudinary Upload API](image_upload_api_reference), then [reference the file you want to upload](node_image_and_video_upload#server_side_upload). 
|

#### Add callback functions
{table:class=tutorial-bullets}|  | 
| --- | --- |
| | Add your [callback functions](https://nodejs.org/en/learn/asynchronous-work/javascript-asynchronous-programming-and-callbacks#callbacks/). This tutorial [uses promises to handle the successes and failures in the code](https://tpiros.dev/blog/promises-in-javascript/).
|

### Ensure script libraries are installed
{table:class=tutorial-bullets}|  | 
| --- | --- |
| | Make sure all of our script's libraries [are properly installed](https://docs.npmjs.com/cli/v7/commands/npm-install) with a simple `npm i` command. If you opening your **package.json** file, you can see all of the packages have been listed as dependencies.
|

#### Run the script and upload the local asset
{table:class=tutorial-bullets}|  | 
| --- | --- |
| | You should have gotten [a successful JSON response](upload_images#upload_response) with lots of data about the uploaded file, including its resolution, file size, format, and more. The file is also now an immediately deliverable asset from a secure, HTTPS URL.
|

#### Upload an asset from a public URL
{table:class=tutorial-bullets}|  | 
| --- | --- |
| | To upload a file from any public URL, [simply enter the full URL of the asset](upload_parameters#upload_from_a_remote_url), instead of the local file path.
|

#### Add parameters to the upload call
{table:class=tutorial-bullets}|  | 
| --- | --- |
| | You can [edit the file's public ID](upload_parameters#public_id), so the asset is named exactly what you want it to be. It is also possible to [add tags to the asset](image_upload_api_reference#tags_method), so you can easily find and deliver it later. You can even [apply quality analysis features](image_quality_analysis) to provide automation, based on the blurriness or overall size of the asset.

## Basic uploading 

You can upload assets programmatically either by using authenticated uploads that include a signature, or using unauthenticated uploads without a signature but with certain restrictions for security reasons. 

The `upload` API method enables you to upload files with a direct call to Cloudinary by sending an HTTPS POST request to the following Cloudinary REST API URL:

`https://api.cloudinary.com/v1_1/<cloud name>/<resource_type>/upload`

**Where:**

   * `cloud name` is the name of your Cloudinary product environment.
   * `resource_type` is the type of file to upload. Valid values: `image`, `raw`, `video` and `auto` to automatically detect the file type.

For example, to upload an image file to the Cloudinary 'demo' product environment, send an HTTPS POST request to the following URL:

`https://api.cloudinary.com/v1_1/demo/image/upload`

The contents of the POST request you send to Cloudinary depends on whether or not you are making an [authenticated request](#authenticated_requests) or an [unauthenticated request](#unauthenticated_requests).

Uploading is performed synchronously, and once finished, the uploaded asset is immediately available for transformation and delivery. 

> **TIP**:
>
> :title=Tips

> * Use the [auto-upload](migration#lazy_migration_with_auto_upload) feature for lazy migration of all your assets from a remote location to Cloudinary with minimal effort on your side.

> * [Enterprise](https://cloudinary.com/pricing#pricing-enterprise) customers can set up their account to use an [EU or AP data center](image_upload_api_reference#alternative_data_centers_and_endpoints_premium_feature) with the endpoints becoming `api-eu.cloudinary.com` or `api-ap.cloudinary.com` respectively.

> * Audio files (e.g., MP3, WAV, FLAC) use the `video` asset type, not `raw`. This allows you to apply transformations like trimming, bitrate control, and format conversion. For details, see [Uploading audio files](upload_parameters#uploading_audio_files).

### Authenticated requests

Authenticated upload requests are performed over HTTPS using a secure protocol and include an authentication signature that's generated based on your product environment's `cloud_name`, `api_key` and `api_secret` parameters. 

#### Upload with Cloudinary backend SDKs

Cloudinary's [backend SDKs](backend_sdks) wrap the [Upload endpoint](image_upload_api_reference#upload) and automatically generate the authentication signature based on the product environment credentials provided in your SDK configuration. When using an SDK to upload, the only required parameter is the `file` to upload. 

For more details on uploading with SDKs, see the **Upload** page of the relevant [backend SDK](backend_sdks) guide.


**Upload SDK syntax**

The Cloudinary `upload` SDK methods perform authenticated upload API calls over HTTPS using the following syntax:
 
```multi
|ruby 
Cloudinary::Uploader.upload(file, options = {})
  
|php_2
$cloudinary->uploadApi()->upload($file, $options = []);

|python
cloudinary.uploader.upload(file, **options)

|nodejs
cloudinary.v2.uploader.upload(file, options).then(callback);
  
|java
cloudinary.uploader().upload(Object file, Map options);

|csharp
cloudinary.Upload(UploadParams params); 

|go
resp, err := cld.Upload.Upload(ctx, file, uploader.UploadParams{})

|android
MediaManager.get().upload(file).dispatch();

|swift
cloudinary.createUploader().signedUpload(url: file, params: params) 

```

For example, a simple upload of the `sample.jpg` file:

```multi
|ruby 
Cloudinary::Uploader.upload("sample.jpg")
  
|php_2
$cloudinary->uploadApi()->upload("sample.jpg");
  
|python
cloudinary.uploader.upload("sample.jpg")

|nodejs
cloudinary.v2.uploader
.upload("sample.jpg")
.then(result=>console.log(result)); 
  
|java
cloudinary.uploader().upload("sample.jpg", ObjectUtils.emptyMap());

|csharp
var uploadParams = new ImageUploadParams(){
  File = new FileDescription(@"sample.jpg")};
var uploadResult = cloudinary.Upload(uploadParams);

|go
resp, err := cld.Upload.Upload(ctx, "sample.jpg", uploader.UploadParams{})

|android
MediaManager.get().upload("sample.jpg").dispatch();

|swift
let params = CLDUploadRequestParams()
var mySig = MyFunction(params)  // your own function that returns a signature generated on your backend
params.setSignature(CLDSignature(signature: mySig.signature, timestamp: mySig.timestamp))
let request = cloudinary.createUploader().signedUpload(
  url: "sample.jpg", params: params) 
```

#### Upload via the REST upload endpoint

You can use Cloudinary's REST API directly for authenticated uploads. The Cloudinary `upload` endpoint performs an authenticated upload API call over HTTPS using the following syntax:

```multi
|curl
https://api.cloudinary.com/v1_1/<cloud name>/<resource_type>/upload -X POST --data '<params>'

```

The Upload API supports multiple authentication methods:

* **[Basic Authentication](#using_basic_authentication)**: Authenticate using your API Key and API Secret via HTTP Basic Auth (no signature calculation required)
* **[Signature-based authentication](#using_signature_based_authentication)**: Manually generate a SHA signature with timestamp

> **INFO**: You should never expose your `api_secret` in client-side code, regardless of which authentication method you use.

##### Using Basic Authentication

Basic Authentication is the simplest way to authenticate Upload API requests from your backend. You provide your API Key and API Secret, and Cloudinary handles the authentication without requiring signature calculation.

**To make an authenticated REST API upload request with Basic Authentication:**

1. **Add your credentials**: Include your API Key and API Secret either in the URL or in an Authorization header.

2. **Add the file parameter**: Specify the file to upload. This can be:
    * The actual data (byte array buffer)
    * The Data URI (Base64 encoded)
    * A remote FTP, HTTP or HTTPS URL of an existing file
    * A private storage bucket (Amazon S3 or Google Cloud Storage) URL of an allowlisted bucket
    
    For more details, see [File source options](upload_parameters#required_file_parameter).

3. **Optionally, add customization parameters**: Include any of the many optional upload parameters available for [customizing your upload](upload_parameters), such as:
    * Naming parameters
    * Tags and metadata (manually specified or automatically generated)
    * Incoming transformations
    * AI-based analysis options
    
    See the [optional upload parameters](image_upload_api_reference#upload_optional_parameters) reference for the full list. 
    
    > **TIP**: Instead of passing optional parameters directly in the upload call, you can define them in a signed [upload preset](upload_presets) and then pass that preset in your upload call.

**Example with Basic Authentication:**

```curl
curl https://<API_KEY>:<API_SECRET>@api.cloudinary.com/v1_1/<CLOUD_NAME>/image/upload \
  -X POST \
  -F "file=sample.jpg" \
  -F "public_id=my_sample" \
  -F "tags=example,basic_auth"
```

Or using the Authorization header:

```curl
curl https://api.cloudinary.com/v1_1/<CLOUD_NAME>/image/upload \
  -X POST \
  -u "<API_KEY>:<API_SECRET>" \
  -F "file=sample.jpg" \
  -F "public_id=my_sample"
```

##### Using signature-based authentication

You can also authenticate using signature-based authentication, which requires [manually generating the signature](authentication_signatures). See the [Client-side uploading](client_side_uploading) documentation for an example [sample app to upload multiple files](client_side_uploading#sample_app_upload_multiple_files_using_a_form_signed) from the client-side while using a backend component to generate the signature.

**To make an authenticated REST API upload request with signature-based authentication:**

1. **Add the file parameter**: Specify the file to upload (see options above).

2. **Add the api_key parameter**: Include the unique API Key of your Cloudinary product environment.

3. **Add the timestamp parameter**: Specify Unix time in seconds of the current time (e.g., 1315060076).

4. **Add the signature parameter**: [Generate a signature](authentication_signatures) of all request parameters including the 'timestamp' parameter and any optional parameters added, but excluding the 'api_key', 'resource_type', 'cloud_name' and 'file' parameters, based on your product environment's API secret. The signature is valid for 1 hour. When no optional upload parameters are part of the signed payload, the string to sign is only the `timestamp` parameter pair. 

5. **Optionally, add customization parameters**: Include any of the optional upload parameters (see above).

**Example with signature-based authentication:**

```curl
curl https://api.cloudinary.com/v1_1/demo/image/upload \
  -X POST \
  --data 'file=sample.jpg&timestamp=173719931&api_key=436464676&signature=a781d61f86a6f818af'
```

### Unauthenticated requests

Unauthenticated upload requests enable you to upload assets without the need to generate an authentication signature on your backend. However, for security reasons, only a small set of upload parameters can be passed directly in an unsigned upload call (see note below). Instead, most unsigned upload options are set via the [unsigned upload preset](upload_presets) that's passed in your unsigned upload call. 

Unsigned uploads require an **unsigned upload preset** (`upload_preset`) and can be triggered from any client that knows the preset name. Treat the preset name as sensitive. If you suspect the preset name was exposed:

* Create a new **unsigned upload preset** with a different name and update your clients to use it. See the Admin API: Create / Update / Delete upload presets.
* Delete any unauthorized assets that may have been uploaded (e.g., by tag or folder).
* Consider switching to **authenticated (signed) uploads** for stronger protection, especially in production.

An **upload preset** defines the upload options to apply to all assets that you upload with that preset specified. You can create multiple upload presets for different use cases. For more information on upload presets, see the [upload preset](upload_presets) guide.

#### Perform an unsigned upload with the SDKs

**To perform an unsigned upload with the SDKs:**

1. **Ensure your SDK is configured with your cloud name**. For details, see the relevant [backend SDK](backend_sdks) guide.
2. **Call the unsigned_upload method**: Use your SDK's `unsigned_upload` method with your file.
3. **Set the upload_preset parameter**: Specify the name of your unsigned upload preset.

For example, to upload the `sample.jpg` file with the `unsigned_1` upload preset using an SDK:

```multi
|ruby
Cloudinary::Uploader.unsigned_upload("sample.jpg", "unsigned_1")

|php_2
$cloudinary->uploadApi()->unsignedUpload("sample.jpg", "unsigned_1");

|python
cloudinary.uploader.unsigned_upload("sample.jpg", "unsigned_1")

|nodejs
cloudinary.v2.uploader
.unsigned_upload("sample.jpg", "unsigned_1")
.then(result=>console.log(result)); 
                           
|java
cloudinary.uploader().unsignedUpload("sample.jpg", "unsigned_1");

|csharp
var uploadParams = new ImageUploadParams(){
  File = new FileDescription(@"sample.jpg"),
  Unsigned = true,
  UploadPreset = "unsigned_1"};
var uploadResult = cloudinary.Upload(uploadParams);

|android
MediaManager.get().upload("sample.jpg")
  .unsigned("unsigned_1").dispatch();

|go
resp, err := cld.Upload.UnsignedUpload(ctx, "sample.jpg", "unsigned_1", uploader.UploadParams{
	Unsigned: api.Bool(true)})

|swift
let request = cloudinary.createUploader().upload(
  url: "sample.jpg", uploadPreset: "unsigned_1")

|cli
cld uploader unsigned_upload "sample.jpg" "unsigned_1"
```

#### Perform an unauthenticated REST API upload request

To make an unauthenticated upload request, include the following required parameters:

1. **Add the file parameter**: Specify the file to upload. This can be:
    * The actual data (byte array buffer)
    * The Data URI (Base64 encoded)
    * A remote FTP, HTTP or HTTPS URL of an existing file
    * A private storage bucket (Amazon S3 or Google Cloud Storage) URL of an allowlisted bucket
    
    For more details, see [File source options](upload_parameters#required_file_parameter).

2. **Add the upload_preset parameter**: Specify the name of an unsigned [upload preset](upload_presets) that you defined for unsigned uploading.

For example: 

```multi
|curl
curl https://api.cloudinary.com/v1_1/demo/image/upload -X POST --data 'file=sample.jpg&upload_preset=unsigned_1'  
```

[Try out unsigned uploads in a sample app](client_side_uploading#sample_app_upload_multiple_files_using_a_form_unsigned)
> **NOTE**:
>
> For security reasons, only [this restricted set](image_upload_api_reference#unsigned_upload_parameters) of parameters can be used in an **unsigned** upload request. All other parameters you want to apply must be defined in the upload preset. For more information, see [Unsigned upload parameters](image_upload_api_reference#unsigned_upload_parameters).
> If a supported parameter is included both in the unsigned upload call and in the unsigned upload preset, the upload preset value usually takes precedence, but there are some exceptions. For details, see [Upload preset precedence](upload_presets#upload_preset_precedence).
### Chunked asset upload

To support the upload of large files, the Cloudinary SDKs include a method which offers a degree of tolerance for network issues. The `upload_large` method uploads a large file to the cloud in chunks, and is required for any files that are larger than 100 MB. This is often relevant for video files, as they tend to have larger files sizes.

> **TIP**: If you can't or don't want to use the SDKs, you can perform [manual chunked uploads using the REST API](#manual_chunked_upload_rest). See also the [Client-side uploading](client_side_uploading) documentation for an example [sample app to upload large files in chunks](client_side_uploading#sample_app_chunked_asset_upload_from_the_client_side) from the client-side.

#### Upload a large file in chunks using an SDK

**To upload a large file using the SDK 'upload_large' method:**

1. **Call the upload_large method**: Use your SDK's `upload_large` method (or equivalent) with your file.

2. **Specify the resource_type parameter**: By default, backend SDKs set the `resource_type` to `image`. Specify the `resource_type` parameter to match your file type. For example, set `resource_type` to `video` for video files or `auto` if you don't know in advance what type to expect. For more details about the `resource_type` option, see [Asset types](upload_parameters#asset_types).

3. **For files larger than 20 GB, set async to true**: Add the `async` parameter set to `true` to upload the file asynchronously.

4. **Optionally, customize the chunk_size**: The default chunk size is 20 MB, but you can set it as low as 5 MB using the `chunk_size` parameter.

For example, uploading a large video file named `my_large_video.mp4`:

```multi
|ruby 
Cloudinary::Uploader.upload_large("my_large_video.mp4", 
    resource_type: "video")

|php_2
// No uploadLarge method. The upload method automatically applies chunked uploading for large files.
$cloudinary->uploadApi()->upload("my_large_video.mp4",
  ["resource_type" => "video"]);     

|python
cloudinary.uploader.upload_large("my_large_video.mp4", 
  resource_type = "video")

|nodejs
cloudinary.v2.uploader
  .upload_large("my_large_video.mp4", { resource_type: "video" })
  .then(result => console.log(result))
  .catch(error => console.error(error));

|java
cloudinary.uploader().uploadLarge("my_large_video.mp4",
  ObjectUtils.asMap("resource_type", "video"));

|csharp
var uploadParams = new VideoUploadParams(){ // This class sets the resource type to Video
  File = new FileDescription(@"my_large_video.mp4")};
var uploadResult = cloudinary.UploadLarge(uploadParams);

|go
// Large files are automatically chunked using the default chunk size.
resp, err := cld.Upload.Upload(ctx, "my_large_video.mp4", uploader.UploadParams{
		ResourceType: "video"}) 

|android
MediaManager.get().upload("my_large_video.mp4")
  .option("resource_type", "video").dispatch();

|swift
let params = CLDUploadRequestParams()
  .setResourseType("video")
var mySig = MyFunction(params)  // your own function that returns a signature generated on your backend
params.setSignature(CLDSignature(signature: mySig.signature, timestamp: mySig.timestamp))
let request = cloudinary.createUploader().signedUploadLarge(
  url: "my_large_video.mp4", params: params)

|cli
cld uploader upload_large "my_large_video.mp4" resource_type="video"
```
  
By default, the chunk size is set to 20 MB but can be set to as low as 5 MB by using the `chunk_size` parameter. For example, uploading a large video file named `my_large_video.mp4` and setting chunk size to 6 MB:

```multi
|ruby  
Cloudinary::Uploader.upload_large("my_large_video.mp4",
  resource_type: "video", 
  chunk_size: 6_000_000)

|php_2
// No distinct uploadLarge method. The upload method automatically applies chunked uploading for large files.
$cloudinary->uploadApi()->upload("my_large_video.mp4", [
    "resource_type" => "video", 
    "chunk_size" => 6000000]);

|python
cloudinary.uploader.upload_large("my_large_video.mp4", 
  resource_type = "video", 
  chunk_size = 6000000)

|nodejs
cloudinary.v2.uploader
  .upload_large("my_large_video.mp4", { resource_type: "video", chunk_size: 6000000 })
  .then(result => console.log(result))
  .catch(error => console.error(error));

|java
cloudinary.uploader().uploadLarge("my_large_video.mp4",
  ObjectUtils.asMap(
    "resource_type", "video", 
    "chunk_size", 6000000));

|csharp
var uploadParams = new ImageUploadParams(){ // This class sets the resource type to Video
  File = new FileDescription(@"my_large_video.mp4")};
var uploadResult = cloudinary.UploadLarge(uploadParams, 6000000);

|go
var largeUploader, _ = uploader.New() 
largeUploader.Config.API.ChunkSize = 6000000

resp, err := cld.Upload.Upload(ctx, "my_large_video.mp4", uploader.UploadParams{
		ResourceType: "video"}) 

|android
MediaManager.get().getCloudinary().uploader().uploadLarge("my_large_video.mp4", options, 6000000);

|swift
let params = CLDUploadRequestParams()
  .setResourceType("video")
var mySig = MyFunction(params)  // your own function that returns a signature generated on your backend
params.setSignature(CLDSignature(signature: mySig.signature, timestamp: mySig.timestamp))
let request = cloudinary.createUploader().signedUploadLarge(
  url: "my_large_video.mp4", params: params, chunkSize: 6000000)

|cli
cld uploader upload_large "my_large_video.mp4" resource_type="video" chunk_size=6000000
```

> **NOTES**:
>
> * There are multiple responses to a chunked upload: one after each chunk that only includes basic information plus the `done : false` parameter, and a full upload response that's returned after the final chunk is uploaded with `done: true` included in the response. 

> * The [Node.js SDK](node_image_and_video_upload#node_js_upload_methods) has several different methods for uploading files, including some that take advantage of Node.js's **stream** functionality.

> * If you have issues with large file uploads, see [troubleshooting large file upload failures](#troubleshooting_large_file_upload_failures).

#### Manual chunked upload (REST)

While we recommend using a Cloudinary SDK for chunked uploads, you can also perform chunked uploads directly against the REST API. When uploading in multiple parts, make sure to:

* Include an `X-Unique-Upload-Id` header with a unique value for the upload, and use the same value for every chunk of that file.
* Include a `Content-Range` header with the location of the chunk in the overall file:
bytes `<start>-<end>/<total>` (for example, bytes `0-5999999/22744222`).
  * If the total size is unknown, set  to -1 for all but the last chunk.
* Use chunk sizes greater than 5 MB for all chunks except the last one.
* Send each chunk as a POST to the Upload API endpoint with your usual upload parameters. For example 

##### Example 1: cURL

**Chunk 1** (bytes `0-5,999,999` of a `22,744,222`-byte file)

```curl
curl -X POST "https://api.cloudinary.com/v1_1/<cloud_name>/upload" \
  -H "X-Unique-Upload-Id: <uuid-for-this-upload>" \\
  -H "Content-Range: bytes 0-5999999/22744222" \
  -F file=@chunk-0 \
  -F api_key=<api_key> \
  -F timestamp=<unix_timestamp> \
  -F signature=<signature>
```

**Chunk 2** (bytes `6,000,000-11,999,999` of a `22,744,222`-byte file)

```curl 
curl -X POST "https://api.cloudinary.com/v1_1/<cloud_name>/upload" \
  -H "X-Unique-Upload-Id: <uuid-for-this-upload>" \
  -H "Content-Range: bytes 6000000-11999999/22744222" \
  -F file=@chunk-1 \
  -F api_key=<api_key> \
  -F timestamp=<unix_timestamp> \
  -F signature=<signature>
```

…repeat for subsequent chunks…

**Final** chunk (include the actual total)

```curl
curl -X POST "https://api.cloudinary.com/v1_1/<cloud_name>/upload" \
  -H "X-Unique-Upload-Id: <uuid-for-this-upload>" \
  -H "Content-Range: bytes 18000000-22744221/22744222" \
  -F file=@chunk-last \
  -F api_key=<api_key> \
  -F timestamp=<unix_timestamp> \
  -F signature=<signature>
```

**Behavior**: You’ll receive an intermediate response after each chunk (with done: false), and a final, full upload response after the last chunk (with done: true). See the SDK examples for additional patterns such as upload_chunked, upload_large, and controlling the chunk_size parameter.

##### Example 2: JavaScript fetch

```javascript
const CHUNK_SIZE = 6 * 1024 * 1024; // >= 5 MB
const uploadId = String(Date.now());

async function uploadInChunks(file) {
  const url = `https://api.cloudinary.com/v1_1/<cloud_name>/<resource_type>/upload`;
  let start = 0;
  while (start < file.size) {
    const end = Math.min(start + CHUNK_SIZE, file.size) - 1;
    const blob = file.slice(start, end + 1);
    const form = new FormData();
    form.append('file', blob);
    // include any other required params (e.g., upload_preset or signature params)
    const headers = {
      'X-Unique-Upload-Id': uploadId,
      'Content-Range': `bytes ${start}-${end}/${file.size}`
    };
    await fetch(url, { method: 'POST', body: form, headers });
    start = end + 1;
  }
}
```

> **TIP**: For a complete working example, see the [chunked asset upload sample app](client_side_uploading#sample_app_chunked_asset_upload_from_the_client_side) in the Client-side uploading documentation.

#### Troubleshooting large file upload failures

If a large upload fails in the Media Library or via the Upload API, check the following:

* **Upload size limits**: Maximum total upload size (regardless of how its chunked) depends on your plan and account configuration.  You can check your current [usage limits](programmable_media_asset_usage_data#usage_limits) in your Console Account Settings. For details on upload limits per plan, see [Cloudinary pricing](https://cloudinary.com/pricing/compare-plans).
* **Chunked uploads for files larger than 100 MB**: Files larger than 100 MB must be uploaded in chunks when uploading via the API. The Media Library and Upload Widget handle chunked uploading automatically.
* **Network stability**: Large uploads are more sensitive to timeouts and interrupted connections. Retry the upload using a stable, high-bandwidth connection.
* **Error responses and logs**: Review the error returned in the Media Library or in the API response to identify whether the failure is related to file size limits, account limits, or an interrupted upload.
* **Rate limits and storage quotas**: Uploads can also fail if you reach a relevant account quota or API rate limit. See [Account management](account_management#rate_limits) and [Programmable Media asset usage data](programmable_media_asset_usage_data#usage_limits).  If you're unexpectedly reaching your account's limits when working with large files, you can [contact support](https://support.cloudinary.com/hc/en-us/requests/new).

## Upload response

A successful upload API call returns a response that includes the HTTP and HTTPS URLs for accessing the uploaded file, as well as additional information regarding the uploaded asset. Among these are the assigned public ID and current version of the asset (used in the [Media Library](https://console.cloudinary.com/console/media_library/search), Admin API, and for building transformation and delivery URLs), the asset's dimensions, the file format and a signature for verifying the response. Depending on the optional parameters passed, the response might also include valuable analysis data such as detected faces, prominent colors, exif and other embedded metadata, quality/accessibility and other sophisticated media analysis data.

The exact keys returned in the response may vary depending on the parameters passed in your upload call. For details on these parameters and the response fields they influence, see the [Upload API parameters table](image_upload_api_reference#upload_optional_parameters).
> **INFO**: Cloudinary may add more fields to API responses and notifications in the future, so please ensure that your response parsing remains forward compatible and won't break as a result of unknown fields.

The following is an example of the JSON response returned:

```json
{
  "asset_id": "3515c6000a548515f1134043f9785c2f",
  "public_id": "gotjephlnz2jgiu20zni",
  "version": 1719307544,
  "version_id": "7d2cc533bee9ff39f7da7414b61fce7e",
  "signature": "d0b1009e3271a942836c25756ce3e04d205bf754",
  "width": 1920,
  "height": 1441,
  "format": "jpg",
  "resource_type": "image",
  "created_at": "2024-06-25T09:25:44Z",
  "tags": [],
  "pages": 1,
  "bytes": 896838,
  "type": "upload",
  "etag": "2a2df1d2d2c3b675521e866599273083",
  "placeholder": false,
  "url": "http://res.cloudinary.com/cld-docs/image/upload/v1719307544/gotjephlnz2jgiu20zni.jpg",
  "secure_url": "https://res.cloudinary.com/cld-docs/image/upload/v1719307544/gotjephlnz2jgiu20zni.jpg",
  "asset_folder": "",
  "display_name": "gotjephlnz2jgiu20zni",
  "original_filename": "sample",
  "api_key": "614335564976464"
}
```

> **NOTE**: Although the URLs returned in the response are given with the [version number](image_transformations#asset_versions), including the version in the delivery URL is optional.

### Error handling

Once the POST request is received and processed by Cloudinary, the Upload API returns the status of requests using one of the following HTTP status codes:

* 200 - OK. Successful.
* 400 - Bad request. Invalid request parameters.
* 401 - Authorization required.
* 403 - Not allowed.
* 404 - Not found.
* 420 - Rate limited.
* 500 - Internal error. Contact support. 

In the case of wrong usage of the API or an error, Cloudinary's response is returned with a non-OK (not 200) HTTP Status code and a body with the following JSON format:

```javascript
{ error: { message: "<< Error explanation here >>" } }
```

### Parallel uploads and rate limiting

Cloudinary’s Upload API accepts one file per request, but you can upload many files in parallel for faster migrations. Begin with a modest concurrency (for example, ~10 simultaneous upload requests) and increase gradually. If you hit account limits and receive HTTP 420 (Rate limited), reduce concurrency and retry the failed requests with exponential backoff.

```nodejs
async function uploadAll(files, limit = 10) {
  const queue = [...files];
  const workers = Array.from({ length: limit }, async function worker() {
    while (queue.length) {
      const file = queue.shift();
      try { await cloudinary.uploader.upload(file); }
      catch (e) {
        if (e.http_code === 420) { /* backoff, requeue */ queue.push(file); await new Promise(r=>setTimeout(r, 1000)); }
        else throw e;
      }
    }
  });
  await Promise.all(workers);
}
```

Adjust the concurrency limit to your account and workload; if rate-limited, slow down and retry.

## Upload considerations

There are various ways to upload your resources to your Cloudinary account. Cloudinary supports making both [authenticated requests](#authenticated_requests) that require a signature generated on your backend, and [unauthenticated requests](#unauthenticated_requests) with a restricted set of supported parameters. 

For most production applications, we recommend:

1. Using Cloudinary backend SDKs for server-side uploads.
2. Using the [Upload Widget](upload_widget) for user-generated content.
3. Using direct browser uploads when you want uploads to bypass your servers.

The following table summarizes the available upload options and the situations where each is most appropriate.

| {table:class=no-borders overview wide-lastcol-40} Option | Description | Considerations |
| --- | --- | --- |
| [Cloudinary backend SDKs](#authenticated_requests) (Recommended for most applications)                                                                          | The Cloudinary backend SDKs wrap the Upload API, including upload handling, signature generation, and response verification.                                                                                                   | ✅ Significantly simplifies upload code compared to directly calling the REST API✅ Automatically generates authentication signatures and validates responses✅ Enables you to code in your chosen language✅ Provides built-in support for uploading large files with chunked uploading    |
| [Upload widget](upload_widget) (Recommended for accepting uploads from end users)                                                                              | An interactive, feature-rich interface you can embed in your website or application to allow users to upload assets directly to Cloudinary.                                                                                    | ✅ No need to develop and maintain your own upload UI✅ Simple to integrate✅ Supports unauthenticated client-side uploads✅ Enables uploads from social media and stock photo accounts                                                                                                     |
| [Direct upload from a browser](client_side_uploading#direct_uploading_from_the_browser_via_a_backend_sdk) (Recommended when uploads should bypass your servers) | The Cloudinary Backend SDKs can automatically add a file input field to your form that uploads files directly to Cloudinary, bypassing your own servers.                                                                       | ✅ Uploads directly to your account without passing through your infrastructure✅ Reduces server bandwidth and processing requirements💡 Requires additional setup and configuration💡 Requires the Cloudinary jQuery plugin                                                              |
| ['upload' endpoint of the REST API](#basic_uploading)                                                                                                           | The `upload` endpoint supports both authenticated and unauthenticated uploads and provides direct access to Upload API functionality.                                                                                          | ✅ Useful when coding in a language not covered by Cloudinary's SDKs✅ Can be used for unauthenticated client-side requests💡 Requires manually generating signatures and validating responses for authenticated uploads💡 Requires more implementation and maintenance than using an SDK |
| [The Cloudinary CLI](cloudinary_cli)                                                                                                                            | The Cloudinary CLI (Command Line Interface) enables you to interact with Cloudinary through the command line and provides additional features and helper commands.                                                             | ✅ Simple to use✅ Useful for quickly uploading assets without setting up a formal coding environment✅ Useful for experimenting with upload parameters and behavior✅ Provides upload-specific helper functions (for example, `sync`) not directly available through other upload options  |
| [Lazy migration](migration#lazy_migration_with_auto_upload)                                                                                                     | Cloudinary's lazy migration with the auto-upload feature enables you to migrate files on demand from a remote location, where each asset is automatically uploaded to Cloudinary the first time its delivery URL is requested. | ✅ Simple to implement✅ Only uploads the assets you actually need💡 Not suitable if all assets must be migrated before a specific deadline                                                                                                                                                    |
| [Media Library](https://console.cloudinary.com/console/media_library/search)                                                                                    | The Media Library in the Cloudinary Console lets you upload assets using drag and drop or the built-in Upload Widget.                                                                                                          | ✅ Simple to use✅ Useful for quickly uploading assets without writing code✅ Useful for experimenting with upload preset behavior💡 Less suitable as a primary upload mechanism than programmatic approaches                                                                              |
| [Integrations](integrations)                                                                                                                                    | Cloudinary provides built-in integrations with many leading eCommerce, CMS, and PIM platforms.                                                                                                                                 | ✅ Enables platform users to upload directly from the platform UI💡 Requires initial setup and configuration by a platform administrator💡 Less suitable as a primary upload mechanism outside the integrated platform                                                                        |
| [Media Library widget](media_library_widget)                                                                                                                    | The Media Library widget enables embedding Cloudinary Media Library capabilities, including uploads, into another application's UI.                                                                                            | ✅ Useful for implementing your own Cloudinary integration💡 Less suitable as a primary upload mechanism at scale                                                                                                                                                                       

> **TIP**:
>
> :title=Tips:

> * [MediaFlows](https://console.cloudinary.com/mediaflows), Cloudinary’s drag-and-drop workflow builder for image and video, offers the option to automate image upload with a low-code implementation. See MediaFlow’s documentation on media upload [here](mediaflows_block_reference#upload_media). 

> * Usage limits for uploading, transforming and delivering files depend on your Cloudinary [plan](https://cloudinary.com/pricing). For details, check the **Account** tab in your Cloudinary Console **Settings**.

## Upload best practices

The Upload API supports hundreds of parameter combinations. The right configuration depends on your use case, but the following practices are recommended for most production environments.

Each example below sets its parameters directly in the upload call, so you can see exactly what the practice does. That's fine for a single upload; once the same settings apply to more than one upload, move them to an upload preset instead.

Ideally, use the [Cloudinary Environment Config MCP server](cloudinary_llm_mcp#mcp_servers) to create that preset, then apply it as shown below. There's no need to put preset-creation code inside your app.

### Choose signed or unsigned uploads

Before uploading assets or creating upload presets, decide whether uploads should be signed or unsigned. This is the first decision every application (or AI agent) needs to make, since it determines how you authenticate and which upload options are available.

* **Use signed uploads** from your backend, where your API secret can generate an authentication signature. They support the full range of upload parameters and are the more secure option.
* **Use unsigned uploads** directly from the client side (browser or mobile app), such as user-generated content, where exposing your API secret isn't safe. They require an unsigned upload preset and support a restricted set of parameters.

```multi
|ruby
# Signed upload - from your backend, where the API secret is available
Cloudinary::Uploader.upload("product.jpg",
  asset_folder: "products")

# Unsigned upload - from the client side, no API secret involved
Cloudinary::Uploader.unsigned_upload("product.jpg", "ugc_unsigned")

|php_2
use Cloudinary\Api\Upload\UploadApi;

// Signed upload - from your backend, where the API secret is available
(new UploadApi())->upload('product.jpg', [
  'asset_folder' => 'products']);

// Unsigned upload - from the client side, no API secret involved
(new UploadApi())->unsignedUpload('product.jpg', 'ugc_unsigned');

|python
# Configure from environment variables - never hard-code the API secret
cloudinary.config(
  cloud_name = os.environ["CLOUDINARY_CLOUD_NAME"],
  api_key = os.environ["CLOUDINARY_API_KEY"],
  api_secret = os.environ["CLOUDINARY_API_SECRET"],
  secure = True)

# Signed upload - from your backend, where the API secret is available
cloudinary.uploader.upload("product.jpg",
  asset_folder = "products")

# Unsigned upload - from the client side, no API secret involved
cloudinary.uploader.unsigned_upload("product.jpg", "ugc_unsigned")

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

// Configure from environment variables - never hard-code the API secret
cloudinary.config({
  cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
  api_key: process.env.CLOUDINARY_API_KEY,
  api_secret: process.env.CLOUDINARY_API_SECRET,
  secure: true});

// Signed upload - from your backend, where the API secret is available
await cloudinary.uploader.upload("product.jpg", {
  asset_folder: "products"});

// Unsigned upload - from the client side, no API secret involved
await cloudinary.uploader.unsigned_upload("product.jpg", "ugc_unsigned");

|java
// Signed upload - from your backend, where the API secret is available
cloudinary.uploader().upload("product.jpg",
  ObjectUtils.asMap("asset_folder", "products"));

// Unsigned upload - from the client side, no API secret involved
cloudinary.uploader().unsignedUpload("product.jpg", "ugc_unsigned", ObjectUtils.emptyMap());

|go
// Signed upload - from your backend, where the API secret is available
cld.Upload.Upload(ctx, "product.jpg", uploader.UploadParams{
  AssetFolder: "products"})

// Unsigned upload - from the client side, no API secret involved
cld.Upload.Upload(ctx, "product.jpg", uploader.UploadParams{
  UploadPreset: "ugc_unsigned"})

```

Signed uploads from a mobile app still need a signature generated on your backend; the unsigned examples above are the direct client-side case.

**Learn how:** [Authenticated requests](#authenticated_requests), [Unauthenticated requests](#unauthenticated_requests)

### Build your upload pipeline around an upload preset

Rather than passing the same options in every request, define one or more [upload presets](upload_presets) as the primary configuration mechanism for your upload pipeline. A preset can define folder structure, naming conventions, metadata enrichment, transformations, moderation rules, access control, and more.

The examples below pass parameters in the upload call so each practice is visible on its own. Once two or more uploads share the same settings, move those settings onto a preset and pass `upload_preset` instead:

```multi
|ruby
# Instead of repeating the same options in every upload call:
Cloudinary::Uploader.upload("product.jpg",
  asset_folder: "products",
  use_filename: true,
  unique_filename: false,
  categorization: "google_tagging",
  auto_tagging: 0.7)

# Configure them once on a preset, and reference it from every call site:
Cloudinary::Uploader.upload("product.jpg",
  upload_preset: "products_signed")

|php_2
// Instead of repeating the same options in every upload call:
(new UploadApi())->upload('product.jpg', [
  'asset_folder' => 'products',
  'use_filename' => true,
  'unique_filename' => false,
  'categorization' => 'google_tagging',
  'auto_tagging' => 0.7]);

// Configure them once on a preset, and reference it from every call site:
(new UploadApi())->upload('product.jpg', [
  'upload_preset' => 'products_signed']);

|python
# Instead of repeating the same options in every upload call:
cloudinary.uploader.upload("product.jpg",
  asset_folder = "products",
  use_filename = True,
  unique_filename = False,
  categorization = "google_tagging",
  auto_tagging = 0.7)

# Configure them once on a preset, and reference it from every call site:
cloudinary.uploader.upload("product.jpg",
  upload_preset = "products_signed")

|nodejs
// Instead of repeating the same options in every upload call:
await cloudinary.uploader.upload("product.jpg", {
  asset_folder: "products",
  use_filename: true,
  unique_filename: false,
  categorization: "google_tagging",
  auto_tagging: 0.7});

// Configure them once on a preset, and reference it from every call site:
await cloudinary.uploader.upload("product.jpg", {
  upload_preset: "products_signed"});

|java
// Instead of repeating the same options in every upload call:
cloudinary.uploader().upload("product.jpg", ObjectUtils.asMap(
  "asset_folder", "products",
  "use_filename", true,
  "unique_filename", false,
  "categorization", "google_tagging",
  "auto_tagging", 0.7));

// Configure them once on a preset, and reference it from every call site:
cloudinary.uploader().upload("product.jpg",
  ObjectUtils.asMap("upload_preset", "products_signed"));

|go
// Instead of repeating the same options in every upload call:
cld.Upload.Upload(ctx, "product.jpg", uploader.UploadParams{
  AssetFolder: "products",
  UseFilename: api.Bool(true),
  UniqueFilename: api.Bool(false),
  Categorization: "google_tagging",
  AutoTagging: 0.7})

// Configure them once on a preset, and reference it from every call site:
cld.Upload.Upload(ctx, "product.jpg", uploader.UploadParams{
  UploadPreset: "products_signed"})

```

A preset is itself either signed or unsigned, so its signing mode must match your upload type: create a **signed** preset for signed uploads and an **unsigned** preset for unsigned uploads.

For unsigned uploads, the preset isn't just a convenience: only a restricted set of parameters can be sent in an unsigned request, so every other setting (moderation, metadata generation, eager transformations) has to come from the preset.

Create different presets for different asset types, such as product images, user-generated content, marketing assets, or videos, in the [Cloudinary Console](https://console.cloudinary.com/settings/upload) or with the [Cloudinary Environment Config MCP server](cloudinary_llm_mcp#mcp_servers), which lets you describe the preset in natural language and reference it directly in your upload code. For example: *"Create a signed upload preset for user-generated content that enables auto-tagging, applies moderation, and stores assets in the `ugc` folder."*

For unsigned presets especially, set `allowed_formats` to restrict accepted file types. Anyone who knows an unsigned preset's name can upload through it, so restricting formats limits unexpected uploads and reduces the impact if the name is exposed.

**Learn how:** [Upload presets](upload_presets)

### Upload once and transform on demand

Avoid uploading multiple copies of the same asset for different sizes or formats. Instead:

* Upload the highest-quality source asset
* Generate optimized delivery URLs dynamically, either on-the-fly or using [eager transformations](eager_and_incoming_transformations)
* Deliver assets with automatic format and quality optimization

This reduces storage and simplifies asset management.

```multi
|ruby
# Upload the high-quality original once
Cloudinary::Uploader.upload("product-original.jpg",
  public_id: "products/hat")

# Build delivery URLs on demand - don't upload a copy per size or format
thumbnail = cloudinary_url("products/hat",
  width: 300, height: 300, crop: :fill, gravity: :auto,
  fetch_format: :auto, quality: :auto)

|php_2
// Upload the high-quality original once
(new UploadApi())->upload('product-original.jpg', [
  'public_id' => 'products/hat']);

// Build delivery URLs on demand - don't upload a copy per size or format
$thumbnail = (new ImageTag('products/hat'))
  ->resize(Resize::fill(300, 300)->gravity(Gravity::autoGravity()))
  ->delivery(Delivery::format(Format::auto()))
  ->delivery(Delivery::quality(Quality::auto()));

|python
# Upload the high-quality original once
cloudinary.uploader.upload("product-original.jpg",
  public_id = "products/hat")

# Build delivery URLs on demand - don't upload a copy per size or format
thumbnail, options = cloudinary_url("products/hat",
  width = 300, height = 300, crop = "fill", gravity = "auto",
  fetch_format = "auto", quality = "auto")

|nodejs
// Upload the high-quality original once
await cloudinary.uploader.upload("product-original.jpg", {
  public_id: "products/hat"});

// Build delivery URLs on demand - don't upload a copy per size or format
const thumbnail = cloudinary.url("products/hat", {
  width: 300, height: 300, crop: "fill", gravity: "auto",
  fetch_format: "auto", quality: "auto"});

|java
// Upload the high-quality original once
cloudinary.uploader().upload("product-original.jpg",
  ObjectUtils.asMap("public_id", "products/hat"));

// Build delivery URLs on demand - don't upload a copy per size or format
String thumbnail = cloudinary.url().transformation(new Transformation()
  .width(300).height(300).crop("fill").gravity("auto")
  .fetchFormat("auto").quality("auto")).generate("products/hat");

|go
// Upload the high-quality original once
cld.Upload.Upload(ctx, "product-original.jpg", uploader.UploadParams{
  PublicID: "products/hat"})

// Build delivery URLs on demand - don't upload a copy per size or format
thumbnail, err := cld.Image("products/hat")
thumbnail.Transformation = "w_300,h_300,c_fill,g_auto,f_auto,q_auto"

```

**Learn how:** [Image transformations](image_transformations), [Video transformations](video_manipulation_and_delivery), [Image optimization](image_optimization), and [Video optimization](video_optimization)

### Generate eager transformations to warm the cache

If a transformation takes a long time to generate, generate it eagerly during upload instead of on the fly, so even the first request is served without the processing delay.

Examples include:

* Product thumbnails
* Gallery images
* Social sharing images
* Mobile-specific image sizes
* Common video renditions
* Transformations on long videos or that require heavy processing, such as AI analysis

Eager transformations reduce first-view latency, since the derived assets already exist by the time they're requested.

```multi
|ruby
result = Cloudinary::Uploader.upload("product.jpg",
  public_id: "products/hat",
  eager: [
    { width: 300, height: 300, crop: :fill, gravity: :auto,
      fetch_format: :auto, quality: :auto },
    { width: 1200, crop: :limit, fetch_format: :auto, quality: :auto }],
  eager_async: true,
  notification_url: "https://example.com/webhooks/cloudinary")

|php_2
$result = (new UploadApi())->upload('product.jpg', [
  'public_id' => 'products/hat',
  'eager' => [
    ['width' => 300, 'height' => 300, 'crop' => 'fill', 'gravity' => 'auto',
     'fetch_format' => 'auto', 'quality' => 'auto'],
    ['width' => 1200, 'crop' => 'limit',
     'fetch_format' => 'auto', 'quality' => 'auto']],
  'eager_async' => true,
  'notification_url' => 'https://example.com/webhooks/cloudinary']);

|python
result = cloudinary.uploader.upload("product.jpg",
  public_id = "products/hat",
  eager = [
    {"width": 300, "height": 300, "crop": "fill", "gravity": "auto",
     "fetch_format": "auto", "quality": "auto"},
    {"width": 1200, "crop": "limit",
     "fetch_format": "auto", "quality": "auto"}],
  eager_async = True,
  notification_url = "https://example.com/webhooks/cloudinary")

|nodejs
const result = await cloudinary.uploader.upload("product.jpg", {
  public_id: "products/hat",
  eager: [
    { width: 300, height: 300, crop: "fill", gravity: "auto",
      fetch_format: "auto", quality: "auto" },
    { width: 1200, crop: "limit", fetch_format: "auto", quality: "auto" }],
  eager_async: true,
  notification_url: "https://example.com/webhooks/cloudinary"});

|java
Map result = cloudinary.uploader().upload("product.jpg", ObjectUtils.asMap(
  "public_id", "products/hat",
  "eager", Arrays.asList(
    ObjectUtils.asMap("width", 300, "height", 300, "crop", "fill", "gravity", "auto",
      "fetch_format", "auto", "quality", "auto"),
    ObjectUtils.asMap("width", 1200, "crop", "limit",
      "fetch_format", "auto", "quality", "auto")),
  "eager_async", true,
  "notification_url", "https://example.com/webhooks/cloudinary"));

|go
resp, err := cld.Upload.Upload(ctx, "product.jpg", uploader.UploadParams{
  PublicID: "products/hat",
  Eager: "w_300,h_300,c_fill,g_auto,f_auto,q_auto|w_1200,c_limit,f_auto,q_auto",
  EagerAsync: api.Bool(true),
  NotificationURL: "https://example.com/webhooks/cloudinary"})

```

Set `eager_async: true` so the upload response isn't held up while the derived
assets generate, and use `notification_url` to learn when they're ready. A
given asset type usually needs the same renditions every time, so move these
onto its preset too.

**Learn how:** [Eager transformations](eager_and_incoming_transformations)

### Use chunked uploads for large files

Use a chunked upload method for files larger than 100 MB. Chunked uploads divide the file into smaller requests, improving reliability for large files and tolerance of unstable network connections.

Some SDKs provide a dedicated method for chunked uploads, for example Ruby's and Python's `upload_large`, Java's `uploadLarge`, and Node.js's `upload_chunked`, `upload_chunked_stream`, and `upload_large`. Others, such as the PHP and Go SDKs, use the regular upload method with the `chunk_size` option, which chunks the file automatically.

Set `resource_type` to match the file type, for example `video` for video files or `raw` for other file types. Some SDK methods default this, so check the SDK docs when choosing between methods.

The default chunk size is 20 MB; reduce it to as little as 5 MB with the `chunk_size` option. Smaller chunks may help on less reliable connections, while larger chunks reduce the number of requests.

For files larger than 20 GB, also set `async` to `true`. Maximum upload sizes depend on your plan and account configuration.

```multi
|ruby
Cloudinary::Uploader.upload_large("training-video.mp4",
  resource_type: :video,
  chunk_size: 20_000_000)

|php_2
// Large files are automatically chunked using the default chunk size
(new UploadApi())->upload('training-video.mp4', [
  'resource_type' => 'video']);

|python
cloudinary.uploader.upload_large("training-video.mp4",
  resource_type = "video",
  chunk_size = 20000000)

|nodejs
await cloudinary.uploader.upload_large("training-video.mp4", {
  resource_type: "video",
  chunk_size: 20000000});

|java
cloudinary.uploader().uploadLarge("training-video.mp4",
  ObjectUtils.asMap("resource_type", "video", "chunk_size", 20000000));

|go
// Large files are automatically chunked using the default chunk size
cld.Upload.Upload(ctx, "training-video.mp4", uploader.UploadParams{
  ResourceType: "video"})

```

Chunked uploads return an intermediate response with `done: false` after each chunk; use the final response, with `done: true`, as the result of the upload.

`chunk_size` is a request-level option rather than a preset setting, so it stays in the upload call even when the rest of your configuration moves onto a preset.

Without an SDK, upload chunks manually with the REST API, including a unique `X-Unique-Upload-Id` header for the upload and a `Content-Range` header per chunk. See [Chunked asset upload](#chunked_asset_upload) and the [client-side chunked upload](client_side_uploading#sample_app_chunked_asset_upload_from_the_client_side) example for a complete implementation.

**Learn how:** [Chunked asset upload](#chunked_asset_upload)

### Automatically enrich assets with AI-generated metadata

If you plan to search, organize, moderate, or automate workflows based on asset content, generate metadata during upload rather than adding it later. Depending on your use case:

* Generate tags using auto-tagging services
* Run image analysis
* Extract colors and quality information
* Detect objects, faces, logos, or categories

This makes assets searchable and ready for downstream workflows as soon as they're uploaded.

```multi
|ruby
result = Cloudinary::Uploader.upload("photo.jpg",
  categorization: "google_tagging",
  auto_tagging: 0.7,
  quality_analysis: true,
  colors: true,
  context: { alt: "Blue wool hat", owner_id: "u_1234" })

# result["tags"] is populated as soon as the upload returns

|php_2
$result = (new UploadApi())->upload('photo.jpg', [
  'categorization' => 'google_tagging',
  'auto_tagging' => 0.7,
  'quality_analysis' => true,
  'colors' => true,
  'context' => ['alt' => 'Blue wool hat', 'owner_id' => 'u_1234']]);

|python
result = cloudinary.uploader.upload("photo.jpg",
  categorization = "google_tagging",
  auto_tagging = 0.7,
  quality_analysis = True,
  colors = True,
  context = "alt=Blue wool hat|owner_id=u_1234")

# result["tags"] is populated as soon as the upload returns

|nodejs
const result = await cloudinary.uploader.upload("photo.jpg", {
  categorization: "google_tagging",
  auto_tagging: 0.7,
  quality_analysis: true,
  colors: true,
  context: { alt: "Blue wool hat", owner_id: "u_1234" }});

// result.tags is populated as soon as the upload returns

|java
Map result = cloudinary.uploader().upload("photo.jpg", ObjectUtils.asMap(
  "categorization", "google_tagging",
  "auto_tagging", 0.7,
  "quality_analysis", true,
  "colors", true,
  "context", ObjectUtils.asMap("alt", "Blue wool hat", "owner_id", "u_1234")));

// result.get("tags") is populated as soon as the upload returns

|go
resp, err := cld.Upload.Upload(ctx, "photo.jpg", uploader.UploadParams{
  Categorization: "google_tagging",
  AutoTagging: 0.7,
  QualityAnalysis: api.Bool(true),
  Colors: api.Bool(true),
  Context: uploader.CldApiMap{"alt": "Blue wool hat", "owner_id": "u_1234"}})

```

Analysis settings are the same for every asset of a given type, and an unsigned upload can't send these parameters, so they're a natural fit for a preset. `context` usually varies per asset and stays in the upload call.

**Learn how:** [Auto-tagging](auto_tagging_tutorial), [Image analysis](semantic_data_extraction)

### Moderate user-generated content before publishing

For applications that accept user-generated content (UGC), moderation should be a standard part of the upload workflow. Depending on your requirements, it can be used to:

* Detect inappropriate images
* Flag potentially unsafe content
* Route uploads for manual review
* Prevent assets from being published automatically

Applying moderation at upload prevents unwanted content from becoming publicly available.

```multi
|ruby
result = Cloudinary::Uploader.upload(uploaded_file,
  asset_folder: "ugc",
  moderation: "aws_rek",
  notification_url: "https://example.com/webhooks/cloudinary")

# result["moderation_status"] is "pending" until moderation completes

|php_2
$result = (new UploadApi())->upload($uploadedFile, [
  'asset_folder' => 'ugc',
  'moderation' => 'aws_rek',
  'notification_url' => 'https://example.com/webhooks/cloudinary']);

|python
result = cloudinary.uploader.upload(uploaded_file,
  asset_folder = "ugc",
  moderation = "aws_rek",
  notification_url = "https://example.com/webhooks/cloudinary")

# result["moderation_status"] is "pending" until moderation completes

|nodejs
const result = await cloudinary.uploader.upload(uploadedFile, {
  asset_folder: "ugc",
  moderation: "aws_rek",
  notification_url: "https://example.com/webhooks/cloudinary"});

// result.moderation_status is "pending" until moderation completes

|java
Map result = cloudinary.uploader().upload(uploadedFile, ObjectUtils.asMap(
  "asset_folder", "ugc",
  "moderation", "aws_rek",
  "notification_url", "https://example.com/webhooks/cloudinary"));

// result.get("moderation_status") is "pending" until moderation completes

|go
resp, err := cld.Upload.Upload(ctx, uploadedFile, uploader.UploadParams{
  AssetFolder: "ugc",
  Moderation: "aws_rek",
  NotificationURL: "https://example.com/webhooks/cloudinary"})

```

Moderated assets aren't publicly delivered until approved, so treat
`moderation_status` as part of your publishing logic rather than assuming an
uploaded asset is live. UGC is exactly the case where these settings belong on
a preset; if the upload comes straight from the browser, they have to, since
an unsigned request can't send them.

**Learn how:** [Moderate assets](moderate_assets)

### Transcribe and translate video and audio content during upload

For video or audio assets intended for a broad or global audience, generate transcripts, captions, and translations as part of the upload workflow rather than adding them later. Depending on your requirements:

* Generate automatic transcripts and closed captions
* Translate transcripts into multiple languages
* Add subtitles to improve accessibility and searchability

This makes video and audio content accessible and searchable as soon as it's uploaded.

```multi
|ruby
# Generates a transcript plus .srt and .vtt subtitle files for the video
Cloudinary::Uploader.upload("lecture.mp4",
  resource_type: :video,
  raw_convert: "google_speech:srt:vtt",
  notification_url: "https://example.com/webhooks/cloudinary")

|php_2
// Generates a transcript plus .srt and .vtt subtitle files for the video
(new UploadApi())->upload('lecture.mp4', [
  'resource_type' => 'video',
  'raw_convert' => 'google_speech:srt:vtt',
  'notification_url' => 'https://example.com/webhooks/cloudinary']);

|python
# Generates a transcript plus .srt and .vtt subtitle files for the video
cloudinary.uploader.upload("lecture.mp4",
  resource_type = "video",
  raw_convert = "google_speech:srt:vtt",
  notification_url = "https://example.com/webhooks/cloudinary")

|nodejs
// Generates a transcript plus .srt and .vtt subtitle files for the video
await cloudinary.uploader.upload("lecture.mp4", {
  resource_type: "video",
  raw_convert: "google_speech:srt:vtt",
  notification_url: "https://example.com/webhooks/cloudinary"});

|java
// Generates a transcript plus .srt and .vtt subtitle files for the video
cloudinary.uploader().upload("lecture.mp4", ObjectUtils.asMap(
  "resource_type", "video",
  "raw_convert", "google_speech:srt:vtt",
  "notification_url", "https://example.com/webhooks/cloudinary"));

|go
// Generates a transcript plus .srt and .vtt subtitle files for the video
cld.Upload.Upload(ctx, "lecture.mp4", uploader.UploadParams{
  ResourceType: "video",
  RawConvert: "google_speech:srt:vtt",
  NotificationURL: "https://example.com/webhooks/cloudinary"})

```

Transcription runs asynchronously, so pair it with a `notification_url` rather
than waiting on the upload response. If every video should be transcribed, put
`raw_convert` on your video preset and used a signed upload; an unsigned upload can't send it at all.

**Learn how:** [Video best practices](video_best_practices)

### Apply access control during upload

If assets contain sensitive, licensed, or restricted content, configure access control during upload rather than applying it afterward. Examples include:

* Internal business assets
* User documents
* Licensed media
* Pre-release marketing content

This ensures assets are protected immediately.

```multi
|ruby
result = Cloudinary::Uploader.upload("contract.pdf",
  resource_type: :raw,
  type: "authenticated",
  asset_folder: "documents")

# Deliver it only to authorized users, via a signed URL
url = cloudinary_url(result["public_id"],
  resource_type: :raw, type: "authenticated", sign_url: true)

|php_2
$result = (new UploadApi())->upload('contract.pdf', [
  'resource_type' => 'raw',
  'type' => 'authenticated',
  'asset_folder' => 'documents']);

|python
result = cloudinary.uploader.upload("contract.pdf",
  resource_type = "raw",
  type = "authenticated",
  asset_folder = "documents")

# Deliver it only to authorized users, via a signed URL
url, options = cloudinary_url(result["public_id"],
  resource_type = "raw", type = "authenticated", sign_url = True)

|nodejs
const result = await cloudinary.uploader.upload("contract.pdf", {
  resource_type: "raw",
  type: "authenticated",
  asset_folder: "documents"});

// Deliver it only to authorized users, via a signed URL
const url = cloudinary.url(result.public_id, {
  resource_type: "raw", type: "authenticated", sign_url: true});

|java
Map result = cloudinary.uploader().upload("contract.pdf", ObjectUtils.asMap(
  "resource_type", "raw",
  "type", "authenticated",
  "asset_folder", "documents"));

// Deliver it only to authorized users, via a signed URL
String url = cloudinary.url().resourceType("raw").type("authenticated")
  .signed(true).generate((String) result.get("public_id"));

|go
resp, err := cld.Upload.Upload(ctx, "contract.pdf", uploader.UploadParams{
  ResourceType: "raw",
  Type: "authenticated",
  AssetFolder: "documents"})

// Deliver it only to authorized users, via a signed URL
asset, err := cld.Image(resp.PublicID)
asset.Config.URL.Secure = true
asset.Config.URL.Sign = true

```

Access control set at upload restricts delivery of the asset; your application
still has to decide *who* is authorized before generating the signed URL. If a
whole class of assets is restricted, set the delivery type on their preset
rather than repeating it.

**Learn how:** [Access-controlled assets](control_access_to_media)

### Store assets in meaningful folders

Use `asset_folder` values that reflect your application's structure, for example:

* `products`
* `users`
* `marketing`
* `ugc`
* `support`

A clear folder structure simplifies asset management, permissions, automation, and lifecycle operations.

```multi
|ruby
Cloudinary::Uploader.upload("hat.jpg",
  asset_folder: "products/hats",
  use_filename: true,
  unique_filename: false)

|php_2
(new UploadApi())->upload('hat.jpg', [
  'asset_folder' => 'products/hats',
  'use_filename' => true,
  'unique_filename' => false]);

|python
cloudinary.uploader.upload("hat.jpg",
  asset_folder = "products/hats",
  use_filename = True,
  unique_filename = False)

|nodejs
await cloudinary.uploader.upload("hat.jpg", {
  asset_folder: "products/hats",
  use_filename: true,
  unique_filename: false});

|java
cloudinary.uploader().upload("hat.jpg", ObjectUtils.asMap(
  "asset_folder", "products/hats",
  "use_filename", true,
  "unique_filename", false));

|go
cld.Upload.Upload(ctx, "hat.jpg", uploader.UploadParams{
  AssetFolder: "products/hats",
  UseFilename: api.Bool(true),
  UniqueFilename: api.Bool(false)})

```

When a whole category of assets shares a folder, set `asset_folder` on that
preset; pass it in the call only when the folder varies per upload.

> **NOTE**: `asset_folder` isn't available on product environments using the legacy fixed folder mode. On those environments, use the `folder` parameter instead.

**Learn how:** [Asset folders](upload_parameters#identification)

### Use deterministic asset naming

When assets correspond to known business entities such as products, articles, or user profiles, use meaningful public IDs rather than randomly generated identifiers. Benefits include:

* Predictable URLs
* Easier integrations
* Simpler asset replacement workflows
* Easier debugging and administration

In an upload preset, set `use_filename: true` and `unique_filename: false` to generate consistent public IDs based on the uploaded file's name.

```multi
|ruby
# Name the asset after the business entity it belongs to
Cloudinary::Uploader.upload("hat.jpg",
  public_id: "products/#{product_id}",
  overwrite: true)

# Or derive the public ID from the uploaded file's name
Cloudinary::Uploader.upload("hat.jpg",
  use_filename: true,
  unique_filename: false)

|php_2
// Name the asset after the business entity it belongs to
(new UploadApi())->upload('hat.jpg', [
  'public_id' => "products/{$productId}",
  'overwrite' => true]);

// Or derive the public ID from the uploaded file's name
(new UploadApi())->upload('hat.jpg', [
  'use_filename' => true,
  'unique_filename' => false]);

|python
# Name the asset after the business entity it belongs to
cloudinary.uploader.upload("hat.jpg",
  public_id = f"products/{product_id}",
  overwrite = True)

# Or derive the public ID from the uploaded file's name
cloudinary.uploader.upload("hat.jpg",
  use_filename = True,
  unique_filename = False)

|nodejs
// Name the asset after the business entity it belongs to
await cloudinary.uploader.upload("hat.jpg", {
  public_id: `products/${productId}`,
  overwrite: true});

// Or derive the public ID from the uploaded file's name
await cloudinary.uploader.upload("hat.jpg", {
  use_filename: true,
  unique_filename: false});

|java
// Name the asset after the business entity it belongs to
cloudinary.uploader().upload("hat.jpg", ObjectUtils.asMap(
  "public_id", "products/" + productId,
  "overwrite", true));

// Or derive the public ID from the uploaded file's name
cloudinary.uploader().upload("hat.jpg", ObjectUtils.asMap(
  "use_filename", true,
  "unique_filename", false));

|go
// Name the asset after the business entity it belongs to
cld.Upload.Upload(ctx, "hat.jpg", uploader.UploadParams{
  PublicID: fmt.Sprintf("products/%s", productId),
  Overwrite: api.Bool(true)})

// Or derive the public ID from the uploaded file's name
cld.Upload.Upload(ctx, "hat.jpg", uploader.UploadParams{
  UseFilename: api.Bool(true),
  UniqueFilename: api.Bool(false)})

```

`use_filename` / `unique_filename` is the same for every upload in a pipeline,
so set it on the preset and let each call supply only the entity-specific
`public_id`.

**Learn how:** [Public IDs](upload_parameters#public_id)

### Avoid duplicate uploads

To prevent duplicate uploads: 

* Use a deterministic `public_id` with `overwrite: false` when repeated uploads represent the same logical asset. 
* Use unique public IDs when different assets may have the same filename.

**Learn how:** [Avoiding duplicate uploads](#avoiding_duplicate_uploads), [How can Cloudinary help me to avoid or detect duplicated uploads in my account?](ts_how_can_cloudinary_help_me_to_avoid_or_detect_duplicated_uploads_in_my_account).

### Automate post-upload workflows

If uploads trigger moderation, AI analysis, eager transformations, or other asynchronous processing, use notification URLs to automate downstream workflows rather than polling for results. Common use cases include:

* Updating application records when an upload completes
* Starting approval or review workflows
* Triggering AI analysis pipelines
* Synchronizing external systems

```multi
|ruby
Cloudinary::Uploader.upload("product.jpg",
  moderation: "aws_rek",
  eager: [{ width: 300, height: 300, crop: :fill }],
  eager_async: true,
  notification_url: "https://example.com/webhooks/cloudinary")

|php_2
(new UploadApi())->upload('product.jpg', [
  'moderation' => 'aws_rek',
  'eager' => [['width' => 300, 'height' => 300, 'crop' => 'fill']],
  'eager_async' => true,
  'notification_url' => 'https://example.com/webhooks/cloudinary']);

|python
cloudinary.uploader.upload("product.jpg",
  moderation = "aws_rek",
  eager = [{"width": 300, "height": 300, "crop": "fill"}],
  eager_async = True,
  notification_url = "https://example.com/webhooks/cloudinary")

|nodejs
await cloudinary.uploader.upload("product.jpg", {
  moderation: "aws_rek",
  eager: [{ width: 300, height: 300, crop: "fill" }],
  eager_async: true,
  notification_url: "https://example.com/webhooks/cloudinary"});

|java
cloudinary.uploader().upload("product.jpg", ObjectUtils.asMap(
  "moderation", "aws_rek",
  "eager", Arrays.asList(ObjectUtils.asMap("width", 300, "height", 300, "crop", "fill")),
  "eager_async", true,
  "notification_url", "https://example.com/webhooks/cloudinary"));

|go
cld.Upload.Upload(ctx, "product.jpg", uploader.UploadParams{
  Moderation: "aws_rek",
  Eager: "w_300,h_300,c_fill",
  EagerAsync: api.Bool(true),
  NotificationURL: "https://example.com/webhooks/cloudinary"})

```

Your endpoint receives the result of each asynchronous step, so you can update
records or start a review workflow instead of polling:

```nodejs
app.post("/webhooks/cloudinary", express.json(), (req, res) => {
  const { notification_type, public_id, moderation_status } = req.body;

  if (notification_type === "moderation" && moderation_status === "approved") {
    // publish the asset in your application
  }
  res.sendStatus(200);
});
```

**Learn how:** [Upload notifications](notifications) and [MediaFlows](mediaflows_block_reference#on_asset_upload)

## Common upload patterns

The upload process is often the best place to automate tasks that would otherwise require additional processing later. The following examples show workflows and features that are frequently combined during upload.

| {table:class=no-borders overview wide-lastcol-40} If you're uploading... | Consider adding... | Why |
| --- | --- | --- |
| Product images | Eager background removal, auto-tagging | Creates cleaner storefront images and searchable product catalogs |
| User-generated images | Moderation, auto-tagging, structured metadata | Improves safety, discoverability, and content management |
| User profile photos | Face detection, moderation, eager avatar thumbnails | Produces optimized profile images and safer user experiences |
| Marketing campaign assets | Tags, structured metadata, asset folders | Makes assets easier to find and reuse across teams |
| E-commerce catalogs | Auto-tagging, metadata extraction, eager transformations | Accelerates search, merchandising, and delivery performance |
| Real-estate photos | Auto-tagging, quality analysis, eager responsive variants | Improves listing management and page performance |
| News and editorial images | Metadata extraction, tagging, eager social crops | Simplifies publishing workflows and social sharing |
| Brand assets | Structured metadata, folder organization, access control | Improves governance and asset consistency |
| Community uploads | Moderation, workflow automation, access control | Reduces operational overhead and content-review effort |
| Videos for web delivery | Eager video transcoding, thumbnails, AI analysis | Reduces playback delays and improves viewer experience |
| Training or educational videos | Automatic subtitles, transcripts, eager streaming formats | Improves accessibility and searchability |
| Searchable images and videos | Auto-tagging, color analysis, quality analysis | Enables richer search and filtering capabilities |
| Assets used in AI workflows | Auto-tagging, metadata extraction, structured metadata | Provides additional context for downstream AI applications |

### Example: Product image workflow

A typical product image upload pipeline uploads the original, generates tags
automatically, creates eager background-removed thumbnail derivatives, stores
the asset in a product-specific folder, and attaches product metadata, all in
a single upload call:

```multi
|python
cloudinary.uploader.upload("product-original.jpg",
  public_id = f"products/{product_id}",
  asset_folder = "products/hats",
  overwrite = True,
  categorization = "google_tagging",
  auto_tagging = 0.7,
  context = f"sku={sku}|collection=winter",
  eager = [
    {"effect": "background_removal"},
    {"width": 300, "height": 300, "crop": "fill", "gravity": "auto", "quality": "auto"}],
  eager_async = True,
  notification_url = "https://example.com/webhooks/cloudinary")

|nodejs
await cloudinary.uploader.upload("product-original.jpg", {
  public_id: `products/${productId}`,
  asset_folder: "products/hats",
  overwrite: true,
  categorization: "google_tagging",
  auto_tagging: 0.7,
  context: { sku: sku, collection: "winter" },
  eager: [
    { effect: "background_removal" },
    { width: 300, height: 300, crop: "fill", gravity: "auto", quality: "auto" }],
  eager_async: true,
  notification_url: "https://example.com/webhooks/cloudinary"});

|java
cloudinary.uploader().upload("product-original.jpg", ObjectUtils.asMap(
  "public_id", "products/" + productId,
  "asset_folder", "products/hats",
  "overwrite", true,
  "categorization", "google_tagging",
  "auto_tagging", 0.7,
  "context", ObjectUtils.asMap("sku", sku, "collection", "winter"),
  "eager", Arrays.asList(
    ObjectUtils.asMap("effect", "background_removal"),
    ObjectUtils.asMap("width", 300, "height", 300, "crop", "fill", "gravity", "auto", "quality", "auto")),
  "eager_async", true,
  "notification_url", "https://example.com/webhooks/cloudinary"));

|go
cld.Upload.Upload(ctx, "product-original.jpg", uploader.UploadParams{
  PublicID: fmt.Sprintf("products/%s", productId),
  AssetFolder: "products/hats",
  Overwrite: api.Bool(true),
  Categorization: "google_tagging",
  AutoTagging: 0.7,
  Context: uploader.CldApiMap{"sku": sku, "collection": "winter"},
  Eager: "e_background_removal|w_300,h_300,c_fill,g_auto,q_auto",
  EagerAsync: api.Bool(true),
  NotificationURL: "https://example.com/webhooks/cloudinary"})

```

Every product upload needs this same configuration, so in practice you'd put
all of it except `public_id` and `context` on a `products` preset and call
`upload` with `upload_preset` plus those two values. The asset is then
immediately ready for search, merchandising, optimization, and delivery.

> **NOTE**: Automatic format (`f_auto`) doesn't work as expected in eager transformations. See [Using automatic format in eager transformations](eager_and_incoming_transformations#using_automatic_format_in_eager_transformations).

### Example: User-generated content workflow

A typical user-generated content pipeline uploads the asset, runs automated
moderation, generates tags, stores ownership metadata, and triggers a
downstream review workflow:

```multi
|python
result = cloudinary.uploader.upload(uploaded_file,
  asset_folder = "ugc",
  public_id = f"ugc/{user_id}/{upload_id}",
  overwrite = False,
  moderation = "aws_rek",
  categorization = "google_tagging",
  auto_tagging = 0.7,
  context = f"owner_id={user_id}",
  notification_url = "https://example.com/webhooks/cloudinary")

# result["moderation_status"] is "pending" until moderation completes -
# don't publish the asset until your webhook says it was approved.

|nodejs
const result = await cloudinary.uploader.upload(uploadedFile, {
  asset_folder: "ugc",
  public_id: `ugc/${userId}/${uploadId}`,
  overwrite: false,
  moderation: "aws_rek",
  categorization: "google_tagging",
  auto_tagging: 0.7,
  context: { owner_id: userId },
  notification_url: "https://example.com/webhooks/cloudinary"});

// result.moderation_status is "pending" until moderation completes -
// don't publish the asset until your webhook says it was approved.

|java
Map result = cloudinary.uploader().upload(uploadedFile, ObjectUtils.asMap(
  "asset_folder", "ugc",
  "public_id", "ugc/" + userId + "/" + uploadId,
  "overwrite", false,
  "moderation", "aws_rek",
  "categorization", "google_tagging",
  "auto_tagging", 0.7,
  "context", ObjectUtils.asMap("owner_id", userId),
  "notification_url", "https://example.com/webhooks/cloudinary"));

// result.get("moderation_status") is "pending" until moderation completes -
// don't publish the asset until your webhook says it was approved.

|go
resp, err := cld.Upload.Upload(ctx, uploadedFile, uploader.UploadParams{
  AssetFolder: "ugc",
  PublicID: fmt.Sprintf("ugc/%s/%s", userId, uploadId),
  Overwrite: api.Bool(false),
  Moderation: "aws_rek",
  Categorization: "google_tagging",
  AutoTagging: 0.7,
  Context: uploader.CldApiMap{"owner_id": userId},
  NotificationURL: "https://example.com/webhooks/cloudinary"})

// resp.ModerationStatus is "pending" until moderation completes -
// don't publish the asset until your webhook says it was approved.

```

If the upload comes directly from the browser rather than your backend, none of
these options can be sent in the request. Configure them on an unsigned upload
preset and upload with `upload_preset: "<unsigned_preset_name>"` instead.

## Avoiding duplicate uploads

In many cases, you don’t need to explicitly check whether an asset exists before uploading it. Cloudinary provides upload-time mechanisms that can prevent duplicate uploads without requiring a separate existence check.

Both mechanisms require a deterministic `public_id`: the same value every time you upload a given logical asset. Assign it yourself, or set `use_filename: true` and `unique_filename: false` to derive it from the file's name. A random or auto-generated `public_id` never matches an existing one, so neither `overwrite` setting has any effect.

The behavior then depends on the `overwrite` setting:

* `overwrite: false` keeps the existing asset if the same `public_id` is uploaded again. See [Prevent duplicate uploads during upload](#recommended_prevent_duplicate_uploads_during_upload).

* `overwrite: true` replaces the existing asset if the same `public_id` is uploaded again. See [Replacing existing assets](upload_parameters#replacing_existing_assets).

The `overwrite` setting compares public IDs, not file contents. Two different files can use the same public ID, and the same file can be uploaded with different public IDs.

### Recommended: prevent duplicate uploads during upload

When uploading the same logical asset more than once, use a deterministic `public_id` and set `overwrite` to `false`.

For example, every upload of the product image could use `products/hat` as its public ID. If an asset with that public ID already exists, Cloudinary skips the upload and the response includes:

```json
{
  "existing": true
}
```

This avoids an additional existence-check request and is usually the simplest and most scalable way to prevent repeated uploads.

```multi
|ruby 
Cloudinary::Uploader.upload("hat.jpg", 
  public_id: "products/hat",
  overwrite: false)

|php_2
use Cloudinary\Api\Upload\UploadApi;

(new UploadApi())->upload('hat.jpg', [
  'public_id' => 'products/hat',
  'overwrite' => false]);

|python
cloudinary.uploader.upload("hat.jpg", 
  public_id = "products/hat",
  overwrite = False)

|nodejs
cloudinary.v2.uploader
.upload("hat.jpg", { 
  public_id: "products/hat",
  overwrite: false})
.then(result=>console.log(result));

|java
Map params = ObjectUtils.asMap(
    "public_id", "products/hat",
    "overwrite", false);
Map uploadResult = cloudinary.uploader().upload(new File("hat.jpg"), params);

|csharp
var uploadParams = new ImageUploadParams()
    {
        File = new FileDescription(@"hat.jpg"),   
        PublicId = "products/hat",
        Overwrite = false};
var uploadResult = cloudinary.Upload(uploadParams);

|go
resp, err := cld.Upload.Upload(ctx, "hat.jpg", uploader.UploadParams{
  PublicID: "products/hat",
  Overwrite: api.Bool(false)
})

|android
MediaManager.get().upload("hat.jpg")
   .option("public_id", "products/hat")
   .option("overwrite", "false")
   .dispatch();
```

### Use unique public IDs for different assets

If different assets can have the same filename, don’t use the filename as the public ID and rely on `overwrite` to distinguish them.

For example, two users might both upload `photo.jpg`. If these are separate assets, give each one a unique public ID. You can: 

* Leave `use_filename` unset so Cloudinary assigns a fully random public ID unrelated to the filename.
* Set `use_filename: true` together with `unique_filename: true` so Cloudinary appends a random suffix to the filename-based public ID instead.

Otherwise, `overwrite: false` can cause the second upload to be skipped because an unrelated asset already uses the same public ID. `overwrite: true` would replace the existing asset.

### Check whether an asset exists

If your workflow requires a separate existence check, you can use one of the following methods. These methods answer whether an asset is available or exists at a particular public ID; they don't detect whether two files have identical contents.

#### Option 1: Check existence via the delivery URL

You can issue an HTTP HEAD request to the asset’s delivery URL, for example:

```curl
curl -I https://res.cloudinary.com/demo/image/upload/sample.jpg
```

* Returns 200 if the asset exists
* Returns 404 if the asset doesn't exist

> **NOTES**:
>
> * This method is fast and not rate-limited

> * Results may be affected by CDN caching, especially if the asset was recently deleted

> * Response headers count toward your bandwidth quota

> * A HEAD request doesn't consume transformation quotas unless it triggers an auto-upload or fetch

> * Returns 404 for authenticated or private assets too, so this method can't distinguish "doesn't exist" from "exists but isn't publicly accessible"

> * This approach is suitable when performance is critical and eventual consistency is acceptable.

#### Option 2: Use the Admin API for an authoritative check

To definitively verify whether an asset exists, use the Admin API to retrieve the resource by its public ID.

```multi
|curl
curl https://<API_KEY>:<API_SECRET>@api.cloudinary.com/v1_1/<cloud_name>/resources/image/upload/sample
       
|ruby
result = Cloudinary::Api
.resource('sample')

|php_2
$result = $api
->asset("sample");

|python
result = cloudinary\
.api.resource("sample")

|nodejs
cloudinary.v2.api
.resource('sample')
.then(result=>console.log(result)); 

|java
result = api.resource("sample", ObjectUtils.emptyMap());

|csharp
result = cloudinary.GetResource("sample");

|go
resp, err := cld.Admin.Asset(ctx, admin.AssetParams{PublicID: "sample"})

|cli
cld admin resource sample
```

If the asset exists, the API returns the [resource and its data](admin_api#get_the_details_of_a_single_resource_sample_response). If the asset doesn't exist, the API returns a 404 error.

> **NOTES**:
>
> * Results aren't affected by CDN caching

> * Subject to Admin API rate limits

> * If a deleted asset still has a backup available, the response includes a `placeholder: true` field.

#### Option 3: Using the Upload API explicit method (advanced)

Some upload pipelines use the Upload API’s `explicit()` method to check whether an asset exists before performing upload-related actions.

If the asset exists, the call returns the asset data.
If it doesn't exist, the call returns an error.

```multi
|ruby  
result = Cloudinary::Uploader
.explicit("sample", 
  type: "upload")
 
|php_2
$result = $cloudinary->uploadApi()
->explicit("sample", [
    "type" => "upload"]);
 
|python
result = cloudinary.uploader\
.explicit("sample", 
  type = "upload")


|nodejs
cloudinary.v2.uploader
.explicit("sample", 
  { type: "upload"})
.then(result=>console.log(result)); 

  
|java
result = cloudinary.uploader()
.explicit("sample", 
  ObjectUtils.asMap(
    "type", "upload"));

|csharp
var explicitParams = new ExplicitParams("sample"){
  Type = "upload"};
var explicitResult = cloudinary.Explicit(explicitParams);

|go
resp, err := cld.Upload.Explicit(ctx, uploader.ExplicitParams{
    PublicID: "sample", 
    Type: "upload"})

|dart
cloudinary.uploader().explicit(ExplicitParams("sample",
    params: UploadParams(
        type: "upload")));

|swift
let result = cloudinary.createManagementApi().explicit(publicId)

|curl
curl https://api.cloudinary.com/v1_1/cld-docs/image/explicit -X POST --data 'type=upload&public_id=sample&timestamp=173719931&api_key=614335564976464&signature=a788d68f86a6f868af' 

|cli
cld uploader explicit "sample" type="upload"
```

> **NOTES**:
>
> * The Explicit method is primarily intended for applying actions to existing assets (such as eager transformations)

> * This approach may be useful when your upload flow already relies on the Upload API and you require a definitive, non-cached result.

### Detect duplicate or similar files

The methods above detect public ID conflicts. They don’t detect identical files uploaded with different public IDs.

**To identify files after upload:**

* Compare the returned `etag` values to identify exact matches.

* Request `phash: true`, then compare the returned perceptual hashes to identify visually similar images.

These methods can help you clean up duplicates after upload. They don't prevent the upload itself.

For more information, see [How can Cloudinary help me to avoid or detect duplicated uploads in my account?](ts_how_can_cloudinary_help_me_to_avoid_or_detect_duplicated_uploads_in_my_account).

> **See also**:
>
> * [Upload API Reference](image_upload_api_reference): Provides both REST and SDK syntax, parameter details, and examples for all methods of the Upload API.

> * [Upload Add-ons](cloudinary_add_ons): Many of Cloudinary's add-ons can be activated by adding a parameter in your upload call. These add-ons enable you to take advantage of special deep-learning, AI, and other analytical capabilities offered by Cloudinary as well as other vision and image processing partners. 

> * [Asset management](asset_management): Covers options for managing your uploaded assets programmatically, including various CRUD options, backups and version management, notifications and webhooks, and authentication and signature options.* [User-generated content guide](user_generated_content): Upload is a key component of allowing user-generated content to be displayed on your site. Learn about all the features you can take advantage of when handling user-generated content.
