How to upload videos to YouTube via API: Long-form, Shorts, and automated publishing

A complete developer guide to the YouTube Data API v3 upload flow — OAuth setup, resumable uploads, quota limits, Shorts requirements, and Python and Node.js examples.

How to upload videos to YouTube via API: Long-form, Shorts, and automated publishing

What you are actually dealing with

The YouTube Data API v3 is Google’s official interface for programmatically managing YouTube content. For video uploads, it surfaces as a single videos.insert endpoint — but calling it correctly requires navigating OAuth verification requirements, Google Cloud project setup, a quota system that severely limits how many uploads you can do per day, and for Shorts, a set of undocumented classification heuristics that determine whether your video ends up on the Shorts shelf.

This guide covers the full upload flow from project setup to live video, with dedicated sections on Shorts-specific requirements and code examples in Python and Node.js.


Google Cloud project setup

Before any API calls can happen, you need a Google Cloud project with the YouTube Data API v3 enabled:

  1. Create a project in the Google Cloud Console and enable the YouTube Data API v3
  2. Configure the OAuth consent screen — set your app name, scopes, and authorized domains. Choose “External” user type if publishing on behalf of other YouTube accounts
  3. Create OAuth 2.0 credentials — generate a client ID and client secret. For server-side applications, select “Web application”; for desktop or CLI tools, select “Desktop app”
  4. Submit for verification — apps requesting sensitive scopes must pass Google’s OAuth verification process

The verification requirement is not optional for production use. Unverified apps are limited to 100 users in testing, and apps created after July 28, 2020 that haven’t passed a compliance audit can only upload videos as private — they cannot publish public content.

The compliance review process involves submitting a detailed description of your use case, providing a demo video of your app’s OAuth flow, and agreeing to the YouTube API Services Terms of Service. Plan for 2–4 weeks.


Required OAuth scopes

YouTube uses OAuth 2.0 scopes to control what your application can do:

ScopeUse case
https://www.googleapis.com/auth/youtube.uploadUpload videos only — use this minimum scope whenever possible
https://www.googleapis.com/auth/youtubeFull account management — needed for updating metadata, setting thumbnails, managing playlists

Request only what you need. Using youtube.upload without the broader youtube scope reduces the attack surface of your OAuth consent and is easier to get through Google’s review process.

The OAuth flow for a server-side application:

  1. Redirect the user to Google’s authorization endpoint with your client_id, redirect_uri, and scope
  2. User grants permission — Google redirects back with an authorization code
  3. Exchange the code for an access token and refresh token via POST to https://oauth2.googleapis.com/token
  4. Store the refresh token; use it to obtain new short-lived access tokens without user interaction

Uploading a video: the resumable upload protocol

Videos are uploaded via videos.insert. For any file larger than 5 MB (which is essentially every video), use the resumable upload protocol. Simple uploads (uploadType=media) have no recovery mechanism — a failed upload means starting over.

The resumable flow is three steps.

Step 1: Initiate the upload session

POST to https://www.googleapis.com/upload/youtube/v3/videos?uploadType=resumable&part=snippet,status:

Authorization: Bearer {ACCESS_TOKEN}
Content-Type: application/json; charset=UTF-8
X-Upload-Content-Type: video/mp4
X-Upload-Content-Length: {FILE_SIZE_IN_BYTES}

Request body:

{
"snippet": {
"title": "Your video title",
"description": "Video description",
"tags": ["tag1", "tag2"],
"categoryId": "22"
},
"status": {
"privacyStatus": "public",
"selfDeclaredMadeForKids": false
}
}

The response contains a Location header with the resumable upload URI. This URI is valid for approximately 7 days, and the session resets on each active upload.

Step 2: Upload the video data

PUT to the session URI with the video binary:

Content-Type: video/mp4
Content-Length: {FILE_SIZE_IN_BYTES}
Content-Range: bytes 0-{LAST_BYTE}/{TOTAL_BYTES}

For chunked uploads (recommended for large files or unreliable connections), split the file into chunks. Each chunk must be a multiple of 256 KB (262,144 bytes), except the final chunk, which can be any size.

Step 3: Handle interruptions

If an upload is interrupted, query the session URI to find out how many bytes were received:

PUT {SESSION_URI}
Content-Range: bytes */{TOTAL_BYTES}
Content-Length: 0

An HTTP 308 response with a Range header tells you exactly which bytes arrived. Resume from the next byte.

Success: HTTP 201 Created with the video resource JSON, including the assigned video ID.


Video metadata reference

FieldRequiredNotes
snippet.titleYesMax 100 characters
snippet.descriptionNoMax 5,000 characters
snippet.categoryIdYesNumeric string — "22" = People & Blogs, "28" = Science & Tech
snippet.tagsNoArray of strings; each tag max 500 characters
status.privacyStatusYes"public", "private", or "unlisted"
status.publishAtNoISO 8601 scheduled publish time; requires privacyStatus: "private"
status.selfDeclaredMadeForKidsYesCOPPA compliance flag
status.embeddableNoDefault true
status.licenseNo"youtube" or "creativeCommon"
status.containsSyntheticMediaNoRequired disclosure for AI-generated content

The notifySubscribers query parameter (default true) controls whether channel subscribers receive a notification. Set notifySubscribers=false for bulk uploads or scheduled content where you do not want to trigger mass notifications.


The quota system

The YouTube Data API v3 does not use simple rate limits. It uses a quota system where each operation costs a fixed number of units, and every Google Cloud project gets a default of 10,000 units per day.

Key costs:

OperationQuota cost
videos.insert (upload)1,600 units
thumbnails.set50 units
videos.update50 units
videos.list1 unit
videos.delete50 units

Upload is by far the most expensive operation. On the default 10,000-unit daily quota, you can upload approximately 6 videos per day (6 × 1,600 = 9,600 units) before exhausting the project’s quota.

Critical quota behavior:

  • All API keys within a single Google Cloud project share the same 10,000-unit pool — creating multiple API keys does not multiply your quota
  • Multiple separate Google Cloud projects each get their own 10,000-unit pool independently
  • Quota resets at midnight Pacific Time daily
  • Quota increases are free: submit a request through the Google Cloud Console, providing documentation of your use case and compliance with the YouTube API Services Terms of Service

Checking processing status

After upload, YouTube transcodes the video and runs it through content review. Until processing is complete, the video is not visible to viewers even if set to public.

Check status with videos.list:

GET https://www.googleapis.com/youtube/v3/videos?id={VIDEO_ID}&part=status,processingDetails

Key fields:

  • status.uploadStatusuploaded, processed, rejected, deleted, or failed
  • processingDetails.processingStatusprocessing, succeeded, failed, or terminated
  • processingDetails.processingProgress.timeLeftMs — estimated milliseconds remaining

Processing time varies: a few minutes for short low-resolution videos, potentially 30+ minutes for 4K or long-form content. Poll the check_after_secs interval if the API returns one; otherwise poll every 30 seconds.


Setting custom thumbnails

After upload completes, set a custom thumbnail via thumbnails.set:

POST https://www.googleapis.com/upload/youtube/v3/thumbnails/set?videoId={VIDEO_ID}

Include the image as the request body. Accepted formats: JPEG, PNG, GIF, BMP. Recommended: 1280×720 pixels minimum, 16:9 aspect ratio. Maximum file size: 2 MB.

Quota cost: 50 units.


YouTube Shorts: requirements and API behavior

There is no dedicated Shorts endpoint. You use the same videos.insert endpoint for Shorts as for regular videos. YouTube classifies a video as a Short based on a combination of signals:

Required for Shorts classification:

  • Duration: 60 seconds or under (YouTube expanded the Shorts shelf to up to 3 minutes in 2024, but under 60 seconds is the well-supported threshold that reliably gets Shorts treatment across all surfaces)
  • Aspect ratio: 9:16 vertical, 1080×1920 recommended
  • Key content should stay within the central 4:5 area to avoid UI element overlap (subscribe buttons, like counter)

How to signal Shorts intent via the API:

  • Include #Shorts in the snippet.title or snippet.description — this is the primary programmatic signal YouTube reads
  • There is no madeForShorts boolean or dedicated parameter in the API

Shorts-specific limitations:

  • Custom thumbnails cannot be set on Shorts — thumbnails.set will return an error. YouTube assigns thumbnails automatically from the video
  • Some metadata fields (chapter markers, end screens) do not apply to Shorts

The classification is ultimately done by YouTube’s backend after processing. Adding #Shorts does not guarantee Shorts shelf placement but strongly signals intent. A vertical sub-60-second video without #Shorts may still be classified as a Short; a horizontal video with #Shorts in the title will not.

Shorts scheduling: Schedule a Short the same way as any video — set status.privacyStatus to "private" and include status.publishAt with the desired publish time.


Common errors

HTTP codeError reasonFix
400invalidValueA metadata field has a bad value — check categoryId, title length, publishAt format
401authErrorAccess token is missing or expired — refresh and retry
403quotaExceededDaily quota exhausted — wait for midnight PT reset
403forbiddenOAuth scope is insufficient or the account is not authorized
403uploadLimitExceededThe channel hit its per-channel upload limit (separate from API quota) — wait 15+ minutes
403privacyErrorApp has not passed compliance audit — can only upload private videos
500/502/503/504backendErrorTransient server error — use exponential backoff

For 5xx errors: retry with exponential backoff starting at 1 second, doubling each retry, capping at 64 seconds, with a maximum of 5 retries.


Python example

import os
import time
from googleapiclient.discovery import build
from googleapiclient.http import MediaFileUpload
from googleapiclient.errors import HttpError
from google_auth_oauthlib.flow import InstalledAppFlow
SCOPES = ["https://www.googleapis.com/auth/youtube.upload"]
RETRIABLE_STATUS_CODES = [500, 502, 503, 504]
MAX_RETRIES = 5
def get_authenticated_service():
flow = InstalledAppFlow.from_client_secrets_file("client_secrets.json", SCOPES)
credentials = flow.run_local_server(port=0)
return build("youtube", "v3", credentials=credentials)
def upload_video(youtube, file_path, title, description, category_id, privacy_status, tags=None):
body = {
"snippet": {
"title": title,
"description": description,
"tags": tags or [],
"categoryId": category_id,
},
"status": {
"privacyStatus": privacy_status,
"selfDeclaredMadeForKids": False,
},
}
media = MediaFileUpload(
file_path,
mimetype="video/*",
resumable=True,
)
insert_request = youtube.videos().insert(
part=",".join(body.keys()),
body=body,
media_body=media,
)
response = None
retry = 0
while response is None:
try:
status, response = insert_request.next_chunk()
if response and "id" in response:
print(f"Upload complete. Video ID: {response['id']}")
return response
except HttpError as e:
if e.resp.status in RETRIABLE_STATUS_CODES:
retry += 1
if retry > MAX_RETRIES:
raise
sleep_time = 2 ** retry
print(f"Retriable error {e.resp.status}. Retrying in {sleep_time}s...")
time.sleep(sleep_time)
else:
raise
if __name__ == "__main__":
youtube = get_authenticated_service()
# Regular long-form video
upload_video(
youtube,
file_path="my_video.mp4",
title="Complete guide to building with YouTube API",
description="A technical walkthrough of the YouTube Data API v3.",
category_id="28", # Science & Technology
privacy_status="public",
tags=["youtube-api", "developer", "tutorial"],
)
# YouTube Short
upload_video(
youtube,
file_path="my_short.mp4", # Vertical 9:16, under 60 seconds
title="Quick tip: YouTube API quotas #Shorts",
description="How YouTube API quotas work in 60 seconds. #Shorts",
category_id="28",
privacy_status="public",
tags=["youtube-shorts", "youtube-api"],
)

Node.js example

const fs = require("fs");
const path = require("path");
const { google } = require("googleapis");
const { authenticate } = require("@google-cloud/local-auth");
const SCOPES = ["https://www.googleapis.com/auth/youtube.upload"];
async function getAuthClient() {
return authenticate({
keyfilePath: path.join(__dirname, "client_secrets.json"),
scopes: SCOPES,
});
}
async function uploadVideo({ filePath, title, description, categoryId, privacyStatus, tags = [] }) {
const auth = await getAuthClient();
const youtube = google.youtube({ version: "v3", auth });
const fileSize = fs.statSync(filePath).size;
const res = await youtube.videos.insert(
{
part: ["snippet", "status"],
requestBody: {
snippet: {
title,
description,
tags,
categoryId,
},
status: {
privacyStatus,
selfDeclaredMadeForKids: false,
},
},
media: {
mimeType: "video/mp4",
body: fs.createReadStream(filePath),
},
},
{
onUploadProgress: (evt) => {
const pct = Math.round((evt.bytesRead / fileSize) * 100);
process.stdout.write(`\rUploading... ${pct}%`);
},
}
);
console.log(`\nUpload complete. Video ID: ${res.data.id}`);
return res.data;
}
// Long-form video
uploadVideo({
filePath: "./my_video.mp4",
title: "Complete guide to the YouTube Data API",
description: "A technical walkthrough for developers.",
categoryId: "28",
privacyStatus: "public",
tags: ["youtube-api", "developer"],
}).catch(console.error);
// YouTube Short — same function, different file and title
uploadVideo({
filePath: "./my_short.mp4", // Vertical 9:16, under 60 seconds
title: "YouTube API quota limits explained #Shorts",
description: "The quota system in 60 seconds. #Shorts",
categoryId: "28",
privacyStatus: "public",
tags: ["youtube-shorts", "youtube-api"],
}).catch(console.error);

The same upload through Postproxy

The YouTube upload flow above involves OAuth consent screen verification, quota management, chunked upload handling, and processing status polling. Here is the same operation through Postproxy:

Terminal window
curl -X POST "https://api.postproxy.dev/api/posts" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"post": {
"body": "Complete guide to building with the YouTube API"
},
"profiles": ["youtube"],
"media": ["https://your-cdn.com/video.mp4"]
}'

For a Short, add platform-specific parameters:

Terminal window
curl -X POST "https://api.postproxy.dev/api/posts" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"post": {
"body": "Quick tip: YouTube API quotas #Shorts"
},
"profiles": ["youtube"],
"media": ["https://your-cdn.com/short.mp4"],
"platforms": {
"youtube": {
"title": "YouTube API quota limits explained #Shorts",
"description": "The quota system in 60 seconds. #Shorts"
}
}
}'

To publish to YouTube alongside other platforms in one request:

Terminal window
curl -X POST "https://api.postproxy.dev/api/posts" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"post": {
"body": "New video out now — link in bio"
},
"profiles": ["youtube", "instagram", "linkedin", "twitter"],
"media": ["https://your-cdn.com/video.mp4"]
}'

What Postproxy handles

Postproxy maintains a verified Google Cloud project with approved OAuth scopes, and handles the entire YouTube upload lifecycle:

  • OAuth consent screen verification and scope approval
  • OAuth 2.0 token exchange and automatic refresh
  • Resumable uploads with automatic retry on interruption
  • Quota monitoring (Postproxy’s verified project has higher quota limits than a default project)
  • Video metadata formatting and category mapping
  • Processing status polling until the video is live
  • Privacy status and scheduling support (scheduled_at timestamps for queued uploads)

Connect your YouTube channel and start publishing through the Postproxy API.

Ready to get started?

Start with our free plan and scale as your needs grow. No credit card required.