What is the YouTube Data API?
The YouTube Data API v3 is the REST API behind almost every YouTube integration: upload tools, analytics dashboards, comment moderators, playlist managers and search widgets. It exposes resources such as videos, channels, playlists, playlistItems, commentThreads, captions, thumbnails and search, each with methods like list, insert, update and delete. Requests go to https://www.googleapis.com/youtube/v3/, and file uploads to https://www.googleapis.com/upload/youtube/v3/.
It is not the only YouTube API. The YouTube Analytics API and Reporting API return channel metrics such as watch time and revenue, and the Live Streaming API manages broadcasts. When people say “the YouTube API”, they usually mean the Data API, and that is what this guide covers.
YouTube API key or OAuth 2.0: which one do you need?
Google offers two kinds of credentials, and the choice depends on whose data you touch:
| You want to | Credential | Example methods |
|---|---|---|
| Read public videos, channels, playlists, comments | API key | videos.list, channels.list, search.list |
| Upload a video | OAuth 2.0 (youtube.upload) | videos.insert |
| Edit metadata, set thumbnails, manage playlists | OAuth 2.0 (youtube or youtube.force-ssl) | videos.update, thumbnails.set, playlistItems.insert |
| Post or moderate comments, upload captions | OAuth 2.0 (youtube.force-ssl) | commentThreads.insert, captions.insert |
| Read a user’s private data (their private videos, subscriptions) | OAuth 2.0 | videos.list with mine=true |
Sources: registering an application, getting started, videos.insert. Read 29 September 2026.
The rule from Google’s getting-started page: operations that insert, update or delete resources always require user authorization. So an API key never uploads anything. One more trap: service accounts, which work for most Google APIs, do not work with YouTube. A service account cannot be linked to a channel, and the call fails with NoLinkedYouTubeAccount (auth guide). Every upload runs on behalf of a real channel owner who clicked “Allow”.
How to get a YouTube API key
- Open the Google Cloud console and create a project (or pick one).
- Go to APIs & Services → Library, search for YouTube Data API v3 and click Enable.
- Go to APIs & Services → Credentials → Create credentials → API key.
- Click the new key and restrict it: limit it to the YouTube Data API v3, and to your server IPs or website referrers. An unrestricted key in a public repo gets its quota burned by someone else.
A key is passed as the key query parameter:
# Public data only: an API key is enough
curl -G "https://www.googleapis.com/youtube/v3/videos" \
-d part=snippet,statistics \
-d id=dQw4w9WgXcQ \
-d key=YOUR_API_KEYOAuth 2.0 for uploads
For uploads, create an OAuth client ID in the same Credentials page (type “Web application” for a server, “Desktop app” for a script), after filling in the OAuth consent screen. Then send the channel owner to Google’s consent page:
https://accounts.google.com/o/oauth2/v2/auth
?client_id=CLIENT_ID.apps.googleusercontent.com
&redirect_uri=https://yourapp.com/callback
&response_type=code
&scope=https://www.googleapis.com/auth/youtube.upload
&access_type=offline
&prompt=consent
&state=RANDOM_STRING access_type=offline is what gets you a refresh token, so your server can keep uploading after the one-hour access token expires. Exchange the returned code, then refresh as needed:
# Code -> access token (about 1 hour) + refresh token
curl -X POST https://oauth2.googleapis.com/token \
-d client_id=CLIENT_ID.apps.googleusercontent.com \
-d client_secret=CLIENT_SECRET \
-d code=AUTH_CODE \
-d grant_type=authorization_code \
-d redirect_uri=https://yourapp.com/callback
# Later: refresh token -> new access token
curl -X POST https://oauth2.googleapis.com/token \
-d client_id=CLIENT_ID.apps.googleusercontent.com \
-d client_secret=CLIENT_SECRET \
-d refresh_token=REFRESH_TOKEN \
-d grant_type=refresh_token
Ask for youtube.upload if all you do is upload. It is the narrowest scope that allows videos.insert. Setting thumbnails, adding to playlists or posting comments needs youtube or youtube.force-ssl, which ask the user for broader access.
YouTube API quota in 2026
This is where most older guides are now wrong. On 1 June 2026 Google moved the Data API to granular quota buckets, starting with videos.insert and search.list (revision history). Until then, an upload cost 1,600 units out of a shared 10,000, which meant a new project could upload about six videos a day. The default allocation is now:
| Quota bucket | Default per project, per day |
|---|---|
Video uploads (videos.insert) | 100 calls, 1 unit each |
Search (search.list) | 100 calls, 1 unit each (every extra page is another call) |
| Everything else | 10,000 units, shared by all other methods |
What the common calls cost from the 10,000-unit bucket:
| Method | Units |
|---|---|
| videos.list, channels.list, playlistItems.list | 1 |
| videos.update | 50 |
| thumbnails.set | 50 |
| playlistItems.insert | 50 |
| commentThreads.insert | 50 |
| captions.insert | 400 |
Sources: quota calculator, videos.insert, revision history. Read 29 September 2026.
Three details that bite in production. Every request costs at least one unit, including invalid ones, so a retry loop on a bad request eats quota. Quotas reset at midnight Pacific Time, not UTC. And quota belongs to the Google Cloud project, not the user: a SaaS with 200 customers shares one pool of 100 uploads a day across all of them. To go higher, you request an extension through the YouTube API Services audit and quota extension form.
The audit: why API uploads stay private
The line at the top of the videos.insert reference is the one to read first: “All videos uploaded via the videos.insert endpoint from unverified API projects created after 28 July 2020 will be restricted to private viewing mode.” Setting privacyStatus to public does not change that, and the call does not fail. The upload succeeds, and the creator later gets an email saying the video is locked as private.
To lift it, the project must pass a compliance audit against the YouTube API Services Terms of Service (audit guide). The same form covers quota extensions. In practice, you describe the app, show how it uses the API, and demonstrate things like how users authorize and how you handle their data. Plan for it early. You can build and test everything while private, but a public launch waits on Google.
How to upload a video with the YouTube API
Use the resumable upload protocol. It survives dropped connections, which matters for files that take minutes to send. videos.insert accepts files up to 256 GB with any video/* MIME type.
1. Start the session, 2. send the file
# 1. Start a resumable session with the metadata
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: 52428800" \
-H "X-Upload-Content-Type: video/mp4" \
-d '{
"snippet": {
"title": "How we cut our build time in half",
"description": "Full write-up: https://example.com/blog/builds",
"tags": ["devops", "ci"],
"categoryId": "28"
},
"status": {
"privacyStatus": "private",
"publishAt": "2026-10-02T15:00:00Z",
"selfDeclaredMadeForKids": false,
"containsSyntheticMedia": false
}
}'
# -> HTTP/1.1 200 OK
# Location: https://www.googleapis.com/upload/youtube/v3/videos?uploadType=resumable&upload_id=SESSION_ID
# 2. Send the bytes to the session URL
curl -X PUT "SESSION_URL" \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: video/mp4" \
--data-binary @video.mp4
# -> 201 Created with the video resource ({"id": "VIDEO_ID", ...})
The first request carries only the metadata and the file size; the session URL comes back in the Location header. The snippet.title is limited to 100 characters, the description to 5,000 bytes and tags to 500 characters in total, commas included, and none of them may contain < or > (video resource).
Resuming an interrupted upload
# Connection dropped? Ask how much YouTube received
curl -i -X PUT "SESSION_URL" \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Length: 0" \
-H "Content-Range: bytes */52428800"
# -> 308 Resume Incomplete
# Range: bytes=0-20971519
# Resume from byte 20971520 with Content-Range: bytes 20971520-52428799/52428800If you upload in chunks instead of one PUT, each chunk except the last must be a multiple of 256 KB and all chunks must be the same size (resumable upload guide). Larger chunks mean fewer requests.
Scheduling with publishAt
The Data API can schedule natively. Upload with status.privacyStatus set to private and status.publishAt set to an ISO 8601 time, as in the example above, and YouTube flips the video to public at that moment. It only works on a private video that has never been published. A time in the past publishes immediately. Remember the audit: for an unverified project the video stays private whatever publishAt says.
Uploading YouTube Shorts via the API
There is no Shorts endpoint and no isShort field. YouTube decides from the file: since 15 October 2024, uploads with a square or vertical aspect ratio that are up to three minutes long are classified as Shorts (YouTube Help). Render at 9:16, keep it at 3:00 or less and upload with videos.insert. A 3:30 vertical video becomes a regular video.
Custom thumbnails
curl -X POST \
"https://www.googleapis.com/upload/youtube/v3/thumbnails/set?videoId=VIDEO_ID" \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: image/jpeg" \
--data-binary @thumbnail.jpg thumbnails.set takes a JPEG or PNG, costs 50 units, and needs the youtube or youtube.force-ssl scope. It returns 403 forbidden when the channel is not allowed to use custom thumbnails (the channel owner has to verify it in YouTube first), and 429 uploadRateLimitExceeded when the channel has set too many recently (thumbnails.set).
YouTube API with Python
Google’s client library handles the resumable protocol for you. This follows the official upload sample, updated to the current google-auth-oauthlib flow, and schedules the video for later:
# pip install google-api-python-client google-auth-oauthlib
import time
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
from googleapiclient.http import MediaFileUpload
SCOPES = ["https://www.googleapis.com/auth/youtube.upload"]
# One-time: opens a browser for the channel owner to consent.
# Store creds.to_json() and reuse the refresh token afterwards.
flow = InstalledAppFlow.from_client_secrets_file("client_secret.json", SCOPES)
creds = flow.run_local_server(port=0)
youtube = build("youtube", "v3", credentials=creds)
body = {
"snippet": {
"title": "3 keyboard shortcuts nobody uses", # max 100 characters
"description": "Links in the comments.", # max 5,000 bytes
"categoryId": "28",
},
"status": {
"privacyStatus": "private",
"publishAt": "2026-10-01T21:00:00Z", # scheduled release
"selfDeclaredMadeForKids": False,
},
}
media = MediaFileUpload("short.mp4", mimetype="video/mp4",
chunksize=-1, resumable=True)
request = youtube.videos().insert(part="snippet,status",
body=body, media_body=media)
response, retry = None, 0
while response is None:
try:
status, response = request.next_chunk()
except HttpError as e:
# Google's sample retries these; 403 quotaExceeded is not retryable
if e.resp.status not in (500, 502, 503, 504) or retry > 5:
raise
retry += 1
time.sleep(2 ** retry)
print("Uploaded:", response["id"])
On a server you will not use run_local_server. Run the web OAuth flow once per channel, store the refresh token, and build Credentials from it on each job.
YouTube API limits at a glance
| Limit | Value |
|---|---|
| Uploads | 100 videos.insert calls per project per day (default) |
| Searches | 100 search.list calls per project per day (default) |
| Other methods | 10,000 units per project per day (default) |
| Quota reset | Midnight Pacific Time |
| File size | 256 GB per video |
| Title | 100 characters |
| Description | 5,000 bytes |
| Tags | 500 characters in total |
| Unaudited project | Uploads locked to private |
| Shorts | Square or vertical, up to 3 minutes |
| Resumable chunks | Multiples of 256 KB, same size except the last |
Sources: quota costs, videos.insert, video resource, resumable uploads, Shorts length. Read 29 September 2026.
Common YouTube API errors and how to fix them
| Error | What Google says | Fix |
|---|---|---|
| 403 quotaExceeded | The request cannot be completed because you have exceeded your quota. | Wait for midnight PT, cut wasted calls (search, failed retries), or request an extension. |
| 400 uploadLimitExceeded | The user has exceeded the number of videos they may upload. | A per-channel limit, not your project quota. Retry later for that channel. |
| 403 forbidden | Access forbidden. The request may not be properly authorized. | Check the token’s scopes and that it belongs to the channel that owns the video. |
| 401 youtubeSignupRequired | The user has a Google Account that is not linked to a YouTube channel. | The user has to create a channel, then authorize again. |
| 400 invalidPublishAt | The request metadata specifies an invalid scheduled publishing time. | Use ISO 8601 and set privacyStatus to private. |
| 400 invalidCategoryId | The snippet.categoryId property specifies an invalid category ID. | Get valid IDs for the region from videoCategories.list. |
| 400 mediaBodyRequired | The request does not include the video content. | Send the file to the /upload/ URL, not the metadata-only endpoint. |
| 403 forbiddenPrivacySetting | The request attempts to set an invalid privacy setting for the video. | Use public, unlisted or private. |
| 429 uploadRateLimitExceeded | The channel has uploaded too many thumbnails recently. | Back off and retry the thumbnails.set call later. |
| NoLinkedYouTubeAccount | Returned when a service account calls the API. | Use OAuth with the channel owner’s account. |
Sources: global errors, videos.insert errors, thumbnails.set errors, authentication. Read 29 September 2026.
One non-error to watch for: an upload that “works” but never goes public. That is the unaudited-project restriction, not a bug in your code.
The shortcut: YouTube uploads with one Upload-Post call
If YouTube is your product, build on the Data API directly. If you just need videos to go out, the three HTTP calls are the easy part. The work is the Google Cloud project, the consent screen, the audit, the refresh tokens, quota planning and retries. Upload-Post’s YouTube API runs that side: you connect a channel once in the dashboard, and uploads go through Upload-Post’s Google project, so you do not need your own audit or quota extension. You send the file itself:
curl -X POST https://api.upload-post.com/api/upload \
-H "Authorization: Apikey YOUR_KEY" \
-F "[email protected]" \
-F "user=my-brand" \
-F "platform[]=youtube" \
-F "title=How we cut our build time in half" \
-F "description=Full write-up: https://example.com/blog/builds" \
-F "privacyStatus=public" \
-F "categoryId=28" \
-F "[email protected]" \
-F "youtube_playlist_id=PLxxxxxxxxxxxx" \
-F "youtube_first_comment=Timestamps below" \
-F "scheduled_date=2026-10-02T11:00:00" \
-F "timezone=America/New_York"
That uploads the video, sets the thumbnail, adds it to a playlist, posts a first comment and publishes at 11:00 New York time on 2 October. The fields map to the Data API’s: privacyStatus, categoryId, tags, embeddable, license, selfDeclaredMadeForKids, containsSyntheticMedia, hasPaidProductPlacement, defaultLanguage, defaultAudioLanguage and allowedCountries / blockedCountries. If you prefer YouTube’s own scheduling, send youtube_publish_at (RFC 3339) instead of scheduled_date. Subtitles go in youtube_subtitle_file with youtube_subtitle_language, with indexed fields for several languages. All parameters are in the Upload Video docs.
The same request can send a Short to TikTok and Instagram Reels at the same time. In Python:
import requests
r = requests.post(
"https://api.upload-post.com/api/upload",
headers={"Authorization": "Apikey YOUR_KEY"},
files={"video": open("short.mp4", "rb")},
data={
"user": "my-brand",
"platform[]": ["youtube", "tiktok", "instagram"],
"title": "3 keyboard shortcuts nobody uses",
"containsSyntheticMedia": "false",
"add_to_queue": "true", # next free slot in your queue
},
)
print(r.json())- Scheduling:
scheduled_datewith atimezone, up to 365 days ahead, oradd_to_queue=truefor the next free slot. - Large files: pass a public URL in
videowithasync_upload=trueand poll Upload Status with therequest_id. - Thumbnails after the fact: Set YouTube Thumbnail replaces the thumbnail of a video that is already live, Shorts included.
- Retries without duplicates: send an
Idempotency-Keyheader and a retried request returns the existing job instead of uploading twice.
The Free plan is free forever with no card: 10 uploads a month and 2 profiles, with YouTube and API access included. Paid plans with unlimited uploads start at $24 a month ($16 billed annually). For step-by-step routes, see how to auto-post YouTube Shorts and how to schedule YouTube videos, or connect an AI assistant through the YouTube MCP server. Posting the same clips to TikTok? Read the TikTok API guide.
Frequently asked questions
Is the YouTube Data API free?
Yes. Google does not charge for the YouTube Data API. Access is limited by quota instead of price: by default a project gets 100 video uploads a day, 100 searches a day and 10,000 units a day for everything else. More quota is free too, but you have to pass a compliance audit to get it.
How do I get a YouTube API key?
In the Google Cloud console, create a project, enable the YouTube Data API v3 under APIs & Services, then go to Credentials and choose Create credentials > API key. Restrict the key to the YouTube Data API and to your server IPs or referrers. The key only works for public data; uploads and anything else that touches a channel need OAuth 2.0.
Can I upload videos to YouTube with an API key?
No. Google’s docs say operations that insert, update or delete resources always require user authorization. videos.insert needs an OAuth 2.0 access token with the youtube.upload scope (or youtube / youtube.force-ssl), granted by the owner of the channel. Service accounts do not work either, because they cannot be linked to a YouTube channel.
How many videos can I upload per day with the YouTube API?
Since June 2026, videos.insert has its own quota bucket: 100 calls per day per Google Cloud project by default, at 1 unit each. Before that change an upload cost 1,600 units from the shared 10,000, which capped a new project at about 6 uploads a day. Individual channels can also hit uploadLimitExceeded, which is a per-channel limit and separate from your project quota.
Why are my API uploads stuck as private?
Because your Google Cloud project has not been audited. Google restricts every video uploaded through videos.insert from an unverified API project created after 28 July 2020 to private viewing. The fix is to submit the YouTube API Services audit form and pass the compliance review. Setting privacyStatus to public does not override it.
How do I upload a YouTube Short through the API?
The same way as any other video. There is no Shorts endpoint or flag: YouTube classifies uploads with a square or vertical aspect ratio of up to three minutes as Shorts. Encode the file at 9:16 (for example 1080×1920), keep it at three minutes or less, and call videos.insert.
Can I schedule a YouTube video with the API?
Yes. Upload the video with status.privacyStatus set to private and status.publishAt set to an ISO 8601 date and time. YouTube makes it public at that moment. It only works on private videos that have never been published, and a time in the past publishes the video right away.
How do I set a custom thumbnail with the API?
Call thumbnails.set with the video ID and a JPEG or PNG image once the upload has finished. It costs 50 units. The channel must be allowed to use custom thumbnails, otherwise the call returns 403 forbidden, and too many thumbnail uploads in a short time return 429 uploadRateLimitExceeded.
How much quota does a full upload use?
The upload itself is 1 call from the Video Uploads bucket. Everything around it comes from the 10,000-unit bucket: thumbnails.set 50, playlistItems.insert 50 per playlist, captions.insert 400 per caption track, commentThreads.insert 50 for a first comment, and videos.update 50 if you change metadata later. An upload with a thumbnail, one playlist and one caption track uses about 550 units of the shared bucket.
Is there a YouTube API for Python?
Yes. Google maintains google-api-python-client, and the official upload sample uses it with MediaFileUpload(resumable=True) and next_chunk() so a dropped connection resumes instead of starting over. Authentication uses google-auth-oauthlib for the OAuth flow. The Python example on this page shows the whole upload.