Tools March 6, 2026

YouTube OAuth Setup Guide for AI Video Matrix Operations

Full walkthrough: Google Cloud project, OAuth consent screen, multi-channel authorization, and a working Python upload script — with every gotcha called out.

W

Woody

woodyrush.com

YouTube OAuth Setup Guide for AI Video Matrix Operations

If you’re building an automated video pipeline — AI-generated scripts, TTS voiceovers, programmatic video rendering, scheduled publishing — you need the YouTube Data API v3 to upload videos without manual interaction.

YouTube’s upload API requires OAuth 2.0 authorization for each channel. This means every channel in your matrix needs to go through a one-time authorization flow that produces a refresh token. Your automation server stores that token and uses it to upload videos on behalf of the channel, 24/7, no browser needed.

This guide walks you through the full setup: creating the Google Cloud project, configuring OAuth, authorizing multiple channels, and verifying uploads work — with all the gotchas called out.

What you’ll end up with:

  • 1 Google Cloud project (shared across all channels)
  • 1 OAuth client (Desktop type)
  • 1 refresh token per YouTube channel, stored in your database
  • A working upload flow you can call from any Python script or task queue

Step 1: Create Google Cloud Project

  1. Go to console.cloud.google.com
  2. Click the project dropdown (top bar) → New Project
  3. Name it something like video-pipeline or yt-uploader
  4. Create → wait 30s → select the new project from the dropdown

One project handles all your channels. You don’t need separate projects per account.

Step 2: Enable YouTube Data API v3

  1. In your project, go to APIs & Services → Library
  2. Search for YouTube Data API v3
  3. Click → Enable
  4. Optionally also enable YouTube Analytics API (useful later for pulling performance metrics)

This is where most people get stuck. Follow exactly:

  1. Go to APIs & Services → OAuth consent screen
  2. Choose External (unless you have Google Workspace, then Internal works)
  3. Fill in:
    • App name: anything descriptive (e.g. Video Uploader) — end users won’t see this
    • User support email: your email
    • Developer contact email: your email
  4. Click Save and Continue

Add Scopes

  1. Click Add or Remove Scopes
  2. Add these three scopes:
    • https://www.googleapis.com/auth/youtube.upload — upload videos
    • https://www.googleapis.com/auth/youtube — manage channel (metadata, playlists)
    • https://www.googleapis.com/auth/youtube.readonly — read channel info
  3. Save and Continue

Add Test Users

  1. Critical step: While your app is in “Testing” status, only explicitly listed test users can complete the OAuth flow. You must add every Google account that owns a YouTube channel in your matrix.
  2. Click Add Users → enter each Google account email
  3. Save and Continue → Back to Dashboard

Testing vs Production Mode

Your app starts in Testing mode. Two things to know:

  • Refresh tokens expire after 7 days in Testing mode. Your automation breaks weekly unless you re-authorize.
  • To fix this permanently, submit for Production verification. Google reviews it (days to weeks). Once approved, refresh tokens don’t expire.
  • For an MVP or initial testing phase, just re-run the auth script when tokens expire. It takes 30 seconds per account.

Step 4: Create OAuth Client Credentials

  1. Go to APIs & Services → Credentials
  2. Click Create Credentials → OAuth client ID
  3. Application type: Desktop app ← do NOT choose “Web application”
  4. Name: video-uploader (or whatever)
  5. Click Create
  6. Download the JSON file → save as client_secret.json in your project directory

This single JSON file is used for all channels. The per-channel differentiation happens at authorization time (Step 5), not here.

Why Desktop app and not Web? The Desktop flow uses a local redirect (localhost) so you don’t need to set up a web server to receive the OAuth callback. You run it locally, approve in the browser, get your token, done.

Step 5: Authorize Each Channel

Run this script once per YouTube account. It opens a browser, you log in with the Google account that owns the channel, grant permissions, and get back a refresh token to store.

Install Dependencies

pip install google-api-python-client google-auth-oauthlib

Authorization Script

"""
youtube_oauth.py — Authorize a YouTube channel for API uploads.
Run once per channel. Log in with the Google account that owns the channel.
Outputs a JSON file with refresh token and channel info.
"""
import json
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build

SCOPES = [
    "https://www.googleapis.com/auth/youtube.upload",
    "https://www.googleapis.com/auth/youtube",
    "https://www.googleapis.com/auth/youtube.readonly",
]

def authorize():
    flow = InstalledAppFlow.from_client_secrets_file(
        "client_secret.json", SCOPES
    )
    # Opens a browser window — log in and grant access
    credentials = flow.run_local_server(port=8090)

    # Verify: fetch channel info to confirm authorization worked
    youtube = build("youtube", "v3", credentials=credentials)
    response = youtube.channels().list(part="snippet", mine=True).execute()

    if not response.get("items"):
        print("ERROR: This Google account has no YouTube channel.")
        print("Create a channel first at https://www.youtube.com/create_channel")
        return

    channel = response["items"][0]
    channel_id = channel["id"]
    channel_title = channel["snippet"]["title"]
    print(f"\n✅ Authorized: {channel_title} ({channel_id})")

    token_data = {
        "channel_id": channel_id,
        "channel_title": channel_title,
        "access_token": credentials.token,
        "refresh_token": credentials.refresh_token,
        "token_uri": credentials.token_uri,
        "client_id": credentials.client_id,
        "client_secret": credentials.client_secret,
        "scopes": list(credentials.scopes),
    }

    filename = f"tokens_{channel_id}.json"
    with open(filename, "w") as f:
        json.dump(token_data, f, indent=2)

    print(f"💾 Tokens saved to {filename}")
    print(f"🔑 Refresh token: {credentials.refresh_token[:20]}...")
    print(f"\nStore this refresh token in your database for automated uploads.")

if __name__ == "__main__":
    authorize()

Repeat for each channel: Run the script, log in with Google account #1, save tokens. Run again, log in with account #2, etc. Each run produces a separate tokens_{channel_id}.json file.

Step 6: Verify Uploads Work

Before wiring this into your pipeline, confirm each channel can actually upload.

Generate a Test Video

# 5-second black screen, silent — just for testing the upload flow
ffmpeg -f lavfi -i color=c=black:s=1080x1920:d=5 \
       -f lavfi -i anullsrc \
       -shortest -c:v libx264 -c:a aac \
       test_video.mp4

Test Upload Script

"""
test_upload.py — Upload a test video to verify OAuth works.
Usage: python test_upload.py tokens_UCxxxx.json test_video.mp4
Uploads as PRIVATE so it won't appear publicly. Delete from YouTube Studio after.
"""
import sys
import json
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
from googleapiclient.http import MediaFileUpload

def test_upload(token_file: str, video_file: str):
    with open(token_file) as f:
        tokens = json.load(f)

    credentials = Credentials(
        token=tokens["access_token"],
        refresh_token=tokens["refresh_token"],
        token_uri=tokens["token_uri"],
        client_id=tokens["client_id"],
        client_secret=tokens["client_secret"],
        scopes=tokens["scopes"],
    )

    youtube = build("youtube", "v3", credentials=credentials)

    body = {
        "snippet": {
            "title": "Test Upload — Delete Me",
            "description": "Automated pipeline test. Safe to delete.",
            "tags": ["test"],
            "categoryId": "28",  # Science & Technology
        },
        "status": {
            "privacyStatus": "private",  # Won't appear publicly
            "selfDeclaredMadeForKids": False,
        },
    }

    media = MediaFileUpload(video_file, chunksize=-1, resumable=True)
    request = youtube.videos().insert(
        part="snippet,status",
        body=body,
        media_body=media,
    )

    response = None
    while response is None:
        status, response = request.next_chunk()
        if status:
            print(f"Uploading... {int(status.progress() * 100)}%")

    video_id = response["id"]
    print(f"\n✅ Upload successful!")
    print(f"🎬 Video ID: {video_id}")
    print(f"🔗 URL: https://youtube.com/watch?v={video_id}")
    print(f"\n(Uploaded as PRIVATE — delete from YouTube Studio when done)")

if __name__ == "__main__":
    if len(sys.argv) != 3:
        print("Usage: python test_upload.py <token_file.json> <video_file.mp4>")
        sys.exit(1)
    test_upload(sys.argv[1], sys.argv[2])

Run for each channel:

python test_upload.py tokens_UCxxxx.json test_video.mp4

If you see “Upload successful” — that channel is ready for automation.


Using Tokens in Your Pipeline

Once tokens are in your database, your upload code reconstructs credentials and auto-refreshes:

from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build

def get_youtube_client(oauth_tokens: dict):
    """Build an authenticated YouTube client from stored tokens."""
    credentials = Credentials(
        token=None,  # Will auto-refresh using refresh_token
        refresh_token=oauth_tokens["refresh_token"],
        token_uri=oauth_tokens.get("token_uri", "https://oauth2.googleapis.com/token"),
        client_id=oauth_tokens["client_id"],
        client_secret=oauth_tokens["client_secret"],
    )
    return build("youtube", "v3", credentials=credentials)

The google-auth library handles access token refresh automatically. As long as the refresh token is valid, you never need to open a browser again.


Quota & Limits

MetricValue
Daily quota per project10,000 units
Cost per video upload~1,600 units
Cost per metadata update~50 units
Safe daily uploads~6 videos
5 accounts × 1 video/day~8,000 units ✅
10 accounts × 2 videos/day~32,000 units ❌ need quota increase

To request more: Google Cloud Console → APIs & Services → YouTube Data API v3 → Quotas → Edit Quotas → fill in justification. Usually approved in 1–3 business days.

All uploads from all channels share the same project quota. Plan accordingly when scaling.


Troubleshooting

“Access blocked: This app’s request is invalid” → You selected “Web application” instead of “Desktop app” in Step 4. Delete the credential and recreate as Desktop.

“The caller does not have permission” → The Google account isn’t listed as a Test User in Step 3. Add it, then retry.

“Token has been expired or revoked” → Your app is in Testing mode and 7 days have passed. Re-run youtube_oauth.py for that account. To fix permanently, submit for Production verification.

“quotaExceeded” → You’ve hit the 10,000 unit daily limit. Resets at midnight Pacific time. Request a quota increase if you need more.

Upload succeeds but video shows “Processing failed” in YouTube Studio → Video file is corrupted or uses an unsupported codec. YouTube requires H.264 video + AAC audio in an MP4 container. Verify with: ffprobe your_video.mp4

“The request metadata specifies an invalid video description” → Description exceeds 5,000 characters, or contains URLs that YouTube blocks. Keep descriptions under 4,000 chars.

“Daily Limit for Unauthenticated Use Exceeded” → Your credentials aren’t being sent correctly. Check that refresh_token is not null in your stored tokens.


Security Notes

  • Never commit client_secret.json or token files to git. Add both patterns to .gitignore.
  • Refresh tokens are sensitive. Treat them like passwords. Store encrypted in your database.
  • One OAuth client for all channels is fine. The per-channel identity comes from the refresh token, not the client.
  • Revoking access: If an account is compromised, revoke at myaccount.google.com/permissions — this invalidates the refresh token immediately.
#youtube #oauth #python #automation #ai-video