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.
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:
- Create a project in the Google Cloud Console and enable the YouTube Data API v3
- 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
- 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”
- 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:
| Scope | Use case |
|---|---|
https://www.googleapis.com/auth/youtube.upload | Upload videos only — use this minimum scope whenever possible |
https://www.googleapis.com/auth/youtube | Full 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:
- Redirect the user to Google’s authorization endpoint with your
client_id,redirect_uri, andscope - User grants permission — Google redirects back with an authorization code
- Exchange the code for an access token and refresh token via POST to
https://oauth2.googleapis.com/token - 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-8X-Upload-Content-Type: video/mp4X-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/mp4Content-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: 0An 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
| Field | Required | Notes |
|---|---|---|
snippet.title | Yes | Max 100 characters |
snippet.description | No | Max 5,000 characters |
snippet.categoryId | Yes | Numeric string — "22" = People & Blogs, "28" = Science & Tech |
snippet.tags | No | Array of strings; each tag max 500 characters |
status.privacyStatus | Yes | "public", "private", or "unlisted" |
status.publishAt | No | ISO 8601 scheduled publish time; requires privacyStatus: "private" |
status.selfDeclaredMadeForKids | Yes | COPPA compliance flag |
status.embeddable | No | Default true |
status.license | No | "youtube" or "creativeCommon" |
status.containsSyntheticMedia | No | Required 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:
| Operation | Quota cost |
|---|---|
videos.insert (upload) | 1,600 units |
thumbnails.set | 50 units |
videos.update | 50 units |
videos.list | 1 unit |
videos.delete | 50 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,processingDetailsKey fields:
status.uploadStatus—uploaded,processed,rejected,deleted, orfailedprocessingDetails.processingStatus—processing,succeeded,failed, orterminatedprocessingDetails.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
#Shortsin thesnippet.titleorsnippet.description— this is the primary programmatic signal YouTube reads - There is no
madeForShortsboolean or dedicated parameter in the API
Shorts-specific limitations:
- Custom thumbnails cannot be set on Shorts —
thumbnails.setwill 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 code | Error reason | Fix |
|---|---|---|
| 400 | invalidValue | A metadata field has a bad value — check categoryId, title length, publishAt format |
| 401 | authError | Access token is missing or expired — refresh and retry |
| 403 | quotaExceeded | Daily quota exhausted — wait for midnight PT reset |
| 403 | forbidden | OAuth scope is insufficient or the account is not authorized |
| 403 | uploadLimitExceeded | The channel hit its per-channel upload limit (separate from API quota) — wait 15+ minutes |
| 403 | privacyError | App has not passed compliance audit — can only upload private videos |
| 500/502/503/504 | backendError | Transient 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 osimport timefrom googleapiclient.discovery import buildfrom googleapiclient.http import MediaFileUploadfrom googleapiclient.errors import HttpErrorfrom 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 videouploadVideo({ 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 titleuploadVideo({ 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:
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:
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:
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_attimestamps for queued uploads)
Connect your YouTube channel and start publishing through the Postproxy API.
Postproxy
One API for every social platform
Publish to Instagram, X, LinkedIn, TikTok, YouTube and more with a single request. Free plan, no credit card required.