---
title: Direct creator uploads · Cloudflare Stream docs
description: Direct creator uploads let your end users upload videos directly to
  Cloudflare Stream without exposing your API token to clients.
lastUpdated: 2024-12-16T22:33:26.000Z
chatbotDeprioritize: false
source_url:
  html: https://developers.cloudflare.com/stream/uploading-videos/direct-creator-uploads/
  md: https://developers.cloudflare.com/stream/uploading-videos/direct-creator-uploads/index.md
---

Direct creator uploads let your end users upload videos directly to Cloudflare Stream without exposing your API token to clients.

* If your video is a [basic upload](https://developers.cloudflare.com/stream/uploading-videos/direct-creator-uploads/#basic-uploads) under 200 MB and users do not need resumable uploads, generate a URL that accepts an HTTP post request.

* If your video is over 200 MB or if you need to allow users to [resume interrupted uploads](https://developers.cloudflare.com/stream/uploading-videos/direct-creator-uploads/#resumable-uploads), generate a URL using the tus protocol.

In either case, you must specify a maximum duration to reserve for the user's upload to ensure it can be accommodated within your available storage.

## Basic uploads

Use this option if your users upload videos under 200 MB, and you do not need to allow resumable uploads.

1. Generate a unique, one-time upload URL using the [Direct upload API](https://developers.cloudflare.com/api/resources/stream/subresources/direct_upload/methods/create/).

```sh
curl https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/direct_upload \
--header 'Authorization: Bearer <API_TOKEN>' \
 --data '{
    "maxDurationSeconds": 3600
 }'
```

```json
{
  "result": {
    "uploadURL": "https://upload.videodelivery.net/f65014bc6ff5419ea86e7972a047ba22",
    "uid": "f65014bc6ff5419ea86e7972a047ba22"
  },
  "success": true,
  "errors": [],
  "messages": []
}
```

1. With the `uploadURL` from the previous step, users can upload video files that are limited to 200 MB in size. Refer to the example request below.

```bash
curl --request POST \
  --form file=@/Users/mickie/Downloads/example_video.mp4 \
  https://upload.videodelivery.net/f65014bc6ff5419ea86e7972a047ba22
```

A successful upload will receive a `200` HTTP status code response. If the upload does not meet the upload constraints defined at time of creation or is larger than 200 MB in size, you will receive a `4xx` HTTP status code response.

## Resumable uploads

1. Create your own API endpoint that returns an upload URL.

The example below shows how to build a Worker to get a URL you can use to upload your video. The one-time upload URL is returned in the `Location` header of the response, not in the response body.

```javascript
export async function onRequest(context) {
  const { request, env } = context;
  const { CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN } = env;
  const endpoint = `https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/stream?direct_user=true`;


  const response = await fetch(endpoint, {
    method: "POST",
    headers: {
      Authorization: `bearer ${CLOUDFLARE_API_TOKEN}`,
      "Tus-Resumable": "1.0.0",
      "Upload-Length": request.headers.get("Upload-Length"),
      "Upload-Metadata": request.headers.get("Upload-Metadata"),
    },
  });


  const destination = response.headers.get("Location");


  return new Response(null, {
    headers: {
      "Access-Control-Expose-Headers": "Location",
      "Access-Control-Allow-Headers": "*",
      "Access-Control-Allow-Origin": "*",
      Location: destination,
    },
  });
}
```

1. Use this API endpoint **directly** in your tus client. A common mistake is to extract the upload URL from your new API endpoint, and use this directly. See below for a complete example of how to use the API from Step 1 with the uppy tus client.

```html
<html>
  <head>
    <link
      href="https://releases.transloadit.com/uppy/v3.0.1/uppy.min.css"
      rel="stylesheet"
    />
  </head>
  <body>
    <div id="drag-drop-area" style="height: 300px"></div>
    <div class="for-ProgressBar"></div>
    <button class="upload-button" style="font-size: 30px; margin: 20px">
      Upload
    </button>
    <div class="uploaded-files" style="margin-top: 50px">
      <ol></ol>
    </div>
    <script type="module">
      import {
        Uppy,
        Tus,
        DragDrop,
        ProgressBar,
      } from "https://releases.transloadit.com/uppy/v3.0.1/uppy.min.mjs";


      const uppy = new Uppy({ debug: true, autoProceed: true });


      const onUploadSuccess = (el) => (file, response) => {
        const li = document.createElement("li");
        const a = document.createElement("a");
        a.href = response.uploadURL;
        a.target = "_blank";
        a.appendChild(document.createTextNode(file.name));
        li.appendChild(a);


        document.querySelector(el).appendChild(li);
      };


      uppy
        .use(DragDrop, { target: "#drag-drop-area" })
        .use(Tus, {
          endpoint: "/api/get-upload-url",
          chunkSize: 150 * 1024 * 1024,
        })
        .use(ProgressBar, {
          target: ".for-ProgressBar",
          hideAfterFinish: false,
        })
        .on("upload-success", onUploadSuccess(".uploaded-files ol"));


      const uploadBtn = document.querySelector("button.upload-button");
      uploadBtn.addEventListener("click", () => uppy.upload());
    </script>
  </body>
</html>
```

For more details on using tus and example client code, refer to [Resumable and large files (tus)](https://developers.cloudflare.com/stream/uploading-videos/resumable-uploads/).

## Upload-Metadata header syntax

You can apply the [same constraints](https://developers.cloudflare.com/api/resources/stream/subresources/direct_upload/methods/create/) as Direct Creator Upload via basic upload when using tus. To do so, you must pass the `expiry` and `maxDurationSeconds` as part of the `Upload-Metadata` request header as part of the first request (made by the Worker in the example above.) The `Upload-Metadata` values are ignored from subsequent requests that do the actual file upload.

The `Upload-Metadata` header should contain key-value pairs. The keys are text and the values should be encoded in base64. Separate the key and values by a space, *not* an equal sign. To join multiple key-value pairs, include a comma with no additional spaces.

In the example below, the `Upload-Metadata` header is instructing Stream to only accept uploads with max video duration of 10 minutes, uploaded prior to the expiry timestamp, and to make this video private:

`'Upload-Metadata: maxDurationSeconds NjAw,requiresignedurls,expiry MjAyNC0wMi0yN1QwNzoyMDo1MFo='`

`NjAw` is the base64 encoded value for "600" (or 10 minutes). `MjAyNC0wMi0yN1QwNzoyMDo1MFo=` is the base64 encoded value for "2024-02-27T07:20:50Z" (an RFC3339 format timestamp)

## Track upload progress

After the creation of a unique one-time upload URL, you may wish to retain the unique identifier (`uid`) returned in the response to track the progress of a user's upload.

You can do that two ways:

* [Search for a video](https://developers.cloudflare.com/stream/manage-video-library/searching/) with the UID to check the status.

* [Create a webhook subscription](https://developers.cloudflare.com/stream/manage-video-library/using-webhooks/) to receive notifications about the video status. These notifications include the video's UID.

## Billing considerations

Direct Creator Upload links count towards your storage limit even if your users have not yet uploaded video to this URL. If the link expires before it is used or the upload cannot be processed, the storage reservation will be released. Otherwise, once the upload is encoded, its true duration will be counted toward storage and the reservation will be released.

For a detailed breakdown of pricing and example scenarios, refer to [Pricing](https://developers.cloudflare.com/stream/pricing/).
