# YouTube API Upload Video: Quota, Shorts, and Code

How to upload a video with the YouTube Data API: videos.insert, the youtube.upload scope, resumable uploads, the upload quota, Shorts, and uploads from a URL.

Canonical page: https://www.tryadeli.com/blog/youtube-api-upload-video
Last updated: 2026-10-12

By Mika Reyes, Co-founder, Adeli. Published 2026-10-12.

## The short answer

To upload a video with the YouTube API, call videos.insert on the YouTube Data API v3 with an OAuth token that carries the youtube.upload scope, and send the file as a resumable upload. Each upload costs 1 unit from its own Video Uploads quota bucket, which defaults to 100 uploads a day per project. Shorts have no flag. Adeli wraps the whole flow in one POST request.

You upload a video with the YouTube API by calling **`videos.insert`** on the YouTube Data API v3, authorized with an OAuth 2.0 token from the channel owner. Send the metadata and the file to `https://www.googleapis.com/upload/youtube/v3/videos`, preferably as a resumable upload so a dropped connection doesn't cost you the whole file. Below: the request, the quota, Shorts, uploads from a URL, and the errors to handle.

## How does the YouTube API upload a video?

**One method does it: `videos.insert`.** It takes a `video` resource (title, description, privacy) plus the media file in the same request. Google's [videos.insert reference](https://developers.google.com/youtube/v3/docs/videos/insert) sets the limits:

| | YouTube Data API v3 |
|---|---|
| **Endpoint** | `POST https://www.googleapis.com/upload/youtube/v3/videos` |
| **Required parameter** | `part`, such as `snippet,status` |
| **Max file size** | **256 GB** |
| **Accepted MIME types** | `video/*`, `application/octet-stream` |
| **Quota cost** | **1 unit** in the Video Uploads bucket |
| **Default limit** | **100 calls a day** per project |

If you leave `status.privacyStatus` unset, the video **defaults to public**, though Google notes some uploads default to private based on the creator's account settings. Set it explicitly every time.

## What do you need before you start?

- **A Google Cloud project** with the YouTube Data API v3 enabled.
- **An OAuth 2.0 client ID.** Uploads act on a user's channel, so an API key alone won't work.
- **One upload scope.** `videos.insert` accepts any one of `youtube.upload`, `youtube`, `youtubepartner`, or `youtube.force-ssl`. Google's [upload guide](https://developers.google.com/youtube/v3/guides/uploading_a_video) describes `youtube.upload` as allowing uploads to the authenticated user's channel but no other access, so it's the narrowest choice.
- **A channel.** The Google account that signs in has to own a YouTube channel, or a Brand Account channel.

## Step 1: Start a resumable upload session

**POST the metadata first, and Google answers with a session URI.** The [resumable upload guide](https://developers.google.com/youtube/v3/guides/using_resumable_upload_protocol) uses `uploadType=resumable`:

```bash
curl -i -X POST \
  "https://www.googleapis.com/upload/youtube/v3/videos?uploadType=resumable&part=snippet,status" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json; charset=UTF-8" \
  -H "X-Upload-Content-Length: $FILE_SIZE_BYTES" \
  -H "X-Upload-Content-Type: video/mp4" \
  -d '{
    "snippet": { "title": "Launch demo", "description": "Two-minute walkthrough", "categoryId": "22" },
    "status": { "privacyStatus": "unlisted", "selfDeclaredMadeForKids": false }
  }'
```

A `200` response carries the session URI in its **`Location` header**. Save it. Every later request goes there.

## Step 2: Upload the file

**PUT the raw bytes to the session URI.** `Content-Length` and `Content-Type` must match the values you declared in Step 1:

```bash
curl -X PUT "$SESSION_URI" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Length: $FILE_SIZE_BYTES" \
  -H "Content-Type: video/mp4" \
  --data-binary "@launch-demo.mp4"
```

A **`201 Created`** returns the full `video` resource, including the new video ID.

## Step 3: Resume an interrupted upload

**Ask how many bytes arrived, then send the rest.** Google treats a lost connection or a `500`, `502`, `503`, or `504` as resumable and tells you to retry with exponential backoff.

1. **Check status.** Send an empty `PUT` to the session URI with `Content-Length: 0` and `Content-Range: bytes */$FILE_SIZE_BYTES`.
2. **Read the `308 Resume Incomplete` response.** Its `Range` header, such as `bytes=0-999999`, lists the bytes Google already has. No `Range` header means nothing arrived.
3. **Send the remainder.** `PUT` the bytes from the next byte onward with `Content-Range: bytes 1000000-$LAST_BYTE/$FILE_SIZE_BYTES`.

An expired session URI returns **`404`**, and you start a new session from Step 1.

## What is the YouTube API upload quota?

**100 uploads a day per project, at 1 unit each, in a bucket of their own.** Google's [quota cost page](https://developers.google.com/youtube/v3/determine_quota_cost) gives every project a default of 100 `search.list` calls, **100 `videos.insert` calls**, and **10,000 units a day** for all other endpoints combined. Quotas reset at **midnight Pacific Time**.

Both numbers are recent, so older tutorials may quote different ones. Google's [revision history](https://developers.google.com/youtube/v3/revision_history) shows two moves:

| Date | What changed |
|---|---|
| **December 4, 2025** | An upload's cost dropped from about **1,600 units** to about **100 units** |
| **June 1, 2026** | `videos.insert` and `search.list` moved to their own quota buckets |

Calls around the upload still draw from the 10,000-unit pool. `thumbnails.set` costs **50 units**, and `videos.list`, which you'll poll for processing status, costs **1**. A request that fails still costs at least one unit.

**Need more than 100 a day?** Google requires an audit against the YouTube API Services Terms of Service before it grants extra quota, per its [quota and compliance audits page](https://developers.google.com/youtube/v3/guides/quota_and_compliance_audits). You apply through the Audit and Quota Extension Form. Running out mid-day returns `403 quotaExceeded`.

## What is the YouTube API upload limit?

**There are two: your project's quota and each channel's own cap.** They fail differently, so handle them separately:

| Limit | Applies to | Error |
|---|---|---|
| **Video Uploads quota** | Your Google Cloud project | `403 quotaExceeded` |
| **Channel upload limit** | One YouTube channel | `400 uploadLimitExceeded` |

Google's reference describes `uploadLimitExceeded` only as the user having "exceeded the number of videos they may upload." It doesn't publish the number, so treat it as a retry-later error for that one channel and keep uploading for the others.

## How do I upload YouTube Shorts with the API?

**Upload a Short with `videos.insert`, like any other video.** The API has no Shorts flag or Shorts endpoint, and the `videos.insert` reference doesn't mention Shorts. YouTube decides from the file: its [Shorts upload help](https://support.google.com/youtube/answer/12779649) lists videos **up to 3 minutes** with a **square or vertical aspect ratio**.

**Check the file before upload.** Duration and aspect ratio decide it, so validate both in your pipeline before you spend a quota unit.

## Can the YouTube API upload a video from a URL?

**No. `videos.insert` needs the file bytes in the request.** Send metadata without media and Google returns `mediaBodyRequired`, "The request does not include the video content." To post a video hosted elsewhere, your server has to download it and then stream it into the resumable upload from Step 2.

## Why are my API uploads private?

**Check your request and the channel's settings first.** Since July 2020, Google has locked uploads from unaudited API projects to private until the project passes a YouTube API Services audit. Adeli's YouTube app **has passed that audit**, so uploads through Adeli get the visibility you ask for.

Things to check, in order:

- **`status.privacyStatus`.** Read it back from the response. It's the visibility YouTube applied.
- **`status.publishAt`.** A scheduled video stays private until that time.
- **The creator's account settings.** Google notes these can default an upload to private.

## Is there a faster way to upload videos to YouTube?

**Yes: send one request to an API that already runs the YouTube integration.** [Adeli's YouTube API](https://www.tryadeli.com/product/youtube) holds the Google developer app, carries Google's review, and refreshes each channel's tokens. Your customers connect their own channels through Adeli's hosted Google sign-in, and you never touch an OAuth token.

**Publish from a URL.** [`POST /api/v1/posts`](https://www.tryadeli.com/docs/api/posts/publish-youtube) takes a public HTTPS `video.url` of up to **1 GB**, and Adeli downloads it and does the upload.

```bash
curl https://app.tryadeli.com/api/v1/posts \
  -H "Authorization: Bearer $ADELI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "platform": "youtube",
    "accountId": "'"$ACCOUNT_ID"'",
    "title": "Launch demo",
    "privacy_status": "unlisted",
    "made_for_kids": false,
    "video": { "url": "https://cdn.example.com/launch-demo.mp4" }
  }'
```

What Adeli handles for you:

- **Big files.** [`POST /api/v1/posts/youtube`](https://www.tryadeli.com/docs/api/posts/upload-youtube-video-file) takes a multipart file of up to **4 GiB**, plus an optional thumbnail. A refused thumbnail comes back as a warning, not a failed upload.
- **Shorts.** There's no separate Shorts call. A vertical or square video up to three minutes becomes a Short.
- **Processing status.** The publish returns `202` with a `publishId`. Poll [`GET /api/v1/posts/youtube/status`](https://www.tryadeli.com/docs/api/posts/get-youtube-upload-status) until `status` is `published` or `failed`, with YouTube's `failureReason` when it fails.
- **Requested vs. applied visibility.** The response reports `requestedPrivacy` and `appliedPrivacy`, and sets `privacyRestricted` to `true` when YouTube applied something other than what you asked for.
- **Separate limit errors.** A spent daily quota returns `429 quota_exhausted` with the reset time in the message. A channel at YouTube's own cap returns `422 upload_limit_exceeded`.

Those two errors are kept apart on purpose. A project-wide quota stop and one channel hitting its cap need different handling, and one busy channel shouldn't pause uploads for everyone else.

The same [posting API](https://www.tryadeli.com/social-media-posting-api) publishes to Instagram, TikTok, Facebook, X, and Bluesky. A YouTube channel counts as one connected account, and your **first 3 connected accounts are free**.

## Frequently asked questions

### Is the YouTube upload API free?

Google meters the YouTube Data API in quota units, not money, and its quota docs list no per-call price. A project gets 100 videos.insert calls a day by default. Going past that means passing a compliance audit and requesting a quota extension, which costs engineering time rather than a fee.

### How many videos can I upload a day with the YouTube API?

By default, 100 per Google Cloud project per day. Google moved videos.insert into its own Video Uploads quota bucket in June 2026, where each call costs 1 unit and the bucket resets at midnight Pacific Time. Each channel also has YouTube's own upload limit, which returns uploadLimitExceeded.

### Which OAuth scope do I need to upload to YouTube?

Any one of youtube.upload, youtube, youtubepartner, or youtube.force-ssl authorizes videos.insert. Pick youtube.upload if uploading is all your app does, because Google describes it as allowing uploads but no other kind of access to the channel.

### Can I set a custom thumbnail through the API?

Yes, with a separate thumbnails.set call after the upload, which costs 50 quota units. YouTube allows custom thumbnails only on verified channels, so plan for the call to be refused on new or unverified channels.

## Related Adeli pages

- [YouTube API for Developers](https://www.tryadeli.com/product/youtube.md): Publish, schedule, and read analytics for YouTube through one API. Adeli owns the YouTube developer app, token refresh, and Google review. Free to start.
- [Social Media Posting & Scheduling API](https://www.tryadeli.com/social-media-posting-api.md): Adeli's social media posting and scheduling API publishes to Instagram, TikTok, YouTube, Facebook, X, and Bluesky in one request, now or at a set time. Free to start.
- [All posts on the Adeli blog](https://www.tryadeli.com/blog.md)

## About the author

Mika Reyes is the CEO behind Adeli and a tech and AI content creator. She was previously co-founder and CEO of Parallax, which was acquired by Phantom, and a product lead at LinkedIn. She is a Forbes 30 Under 30 honoree.
