YouTube API OAuth: Client ID, Scopes, Refresh Tokens
The YouTube Data API needs OAuth 2.0 whenever a request touches a user's channel: uploads, edits, comments, private data. An API key only reads public data. Create an OAuth client ID in Google Cloud, configure the consent screen, request a scope such as youtube.upload with offline access, and store the refresh token. Apps left in Testing get refresh tokens that expire after 7 days. Adeli runs this whole flow for you.
YouTube API OAuth is the OAuth 2.0 flow your app runs so a channel owner can grant it access to their YouTube account. You need it for any request that changes a channel or reads private data, and you set it up in Google Cloud with an OAuth client ID, a consent screen, and one or more YouTube scopes. Below: when an API key is enough, the setup steps, why refresh tokens die, and the verification Google asks for once real users sign in.
Do I need OAuth or an API key for the YouTube Data API?
Use an API key for public data and OAuth 2.0 for anything tied to a user's channel. Google's getting started guide says operations that insert, update, or delete resources "always require user authorization," and that unauthorized requests "only retrieve public data."
| Task | API key | OAuth 2.0 |
|---|---|---|
| Look up a public video or channel | Yes | Yes |
| Search public videos | Yes | Yes |
| Upload a video | No | Yes |
| Edit titles, thumbnails, playlists | No | Yes |
| Post or moderate comments | No | Yes |
| Read the signed-in user's private videos | No | Yes |
An API key is a key= query parameter that identifies your Google Cloud project. It never identifies a user, so YouTube has no channel to act on. If your product posts for your customers, you need OAuth.
Service accounts don't fill the gap. Google's authentication overview supports the service account flow only for YouTube content owners managing several channels, on calls that take onBehalfOfContentOwner. Elsewhere it fails with NoLinkedYouTubeAccount.
What do you need before you start?
- A Google Cloud project with the YouTube Data API v3 enabled in the API Library.
- A domain you control, with a home page and a privacy policy on it. Verification later requires both.
- A redirect URI on your server that receives the authorization code, such as
https://app.example.com/oauth/youtube/callback. - A test Google account that owns a YouTube channel. An account without a channel can authorize but has nothing to upload to.
Step 1: Configure the OAuth consent screen
Set the app name, support email, authorized domains, and audience first. The consent screen is what your customers see when Google asks them to approve access.
- In Google Cloud, open Google Auth Platform → Branding and fill in the app name, logo, support email, home page, and privacy policy link.
- Under Audience, choose External if anyone outside your Google Workspace will connect a channel.
- Under Data Access, add the YouTube scopes you plan to request (Step 3).
These menu names follow Google's consent screen setup guide.
The audience setting has a publishing status, and it changes how your tokens behave. Google's app audience page describes two states:
| Publishing status | Who can sign in | Refresh tokens |
|---|---|---|
| Testing | Up to 100 test users you list on the consent screen | Authorizations expire 7 days after consent |
| In production | Any Google account | Long-lived, subject to the expiry rules below |
Testing is fine for a prototype and wrong for anything else. If your users suddenly have to reconnect once a week, check this setting first.
Step 2: Create the OAuth client ID
Create a client of type "Web application" and register your exact redirect URI. Go to Google Auth Platform → Clients → Create client, pick Web application, and add the callback URL under Authorized redirect URIs. Google's web server OAuth guide walks through the same form.
Google gives you a client ID and a client secret. The client ID goes into the authorization URL. The client secret stays on your server and is only used in the token exchange. Never ship it in browser or mobile code.
Step 3: Pick the narrowest YouTube scope
Request only the scopes your feature uses. Google's web server OAuth guide lists the YouTube Data API v3 scopes:
| Scope | What Google's consent screen says |
|---|---|
youtube.upload |
"Manage your YouTube videos" |
youtube.readonly |
"View your YouTube account" |
youtube.force-ssl |
"See, edit, and permanently delete your YouTube videos, ratings, comments and captions" |
youtube |
"Manage your YouTube account" |
youtubepartner |
"View and manage your assets and associated content on YouTube" |
Each scope is prefixed with https://www.googleapis.com/auth/. If uploading is all your app does, youtube.upload is the narrowest choice. If you also post or moderate comments, you need youtube.force-ssl. Our YouTube upload guide covers which scopes videos.insert accepts.
Step 4: Send the user to Google and exchange the code
Redirect the user to Google's authorization endpoint with access_type=offline so you get a refresh token.
https://accounts.google.com/o/oauth2/v2/auth
?client_id=YOUR_CLIENT_ID
&redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth%2Fyoutube%2Fcallback
&response_type=code
&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fyoutube.upload
&access_type=offline
&prompt=consent
&include_granted_scopes=true
&state=RANDOM_STATE_VALUEaccess_type=offlineasks Google for a refresh token along with the access token.prompt=consentshows the consent screen again. Without it, Google prompts only the first time, and a reconnecting user may come back without a new refresh token.include_granted_scopes=trueturns on incremental authorization, so a later scope request keeps the scopes already granted.stateis your CSRF check. Compare it on the callback.
Google sends the user back to your redirect URI with a code. Exchange it on your server:
curl -X POST https://oauth2.googleapis.com/token \
-d client_id="$GOOGLE_CLIENT_ID" \
-d client_secret="$GOOGLE_CLIENT_SECRET" \
-d code="$AUTH_CODE" \
-d grant_type=authorization_code \
-d redirect_uri="https://app.example.com/oauth/youtube/callback"The response carries an access_token, its expires_in in seconds, and a refresh_token. Store the refresh token encrypted, per channel. It's the only thing that lets you act for that user later.
Step 5: Refresh the access token
Access tokens last about an hour, so swap the refresh token for a new one when they expire. The request goes to the same token endpoint:
curl -X POST https://oauth2.googleapis.com/token \
-d client_id="$GOOGLE_CLIENT_ID" \
-d client_secret="$GOOGLE_CLIENT_SECRET" \
-d refresh_token="$REFRESH_TOKEN" \
-d grant_type=refresh_tokenGoogle returns a new access_token and expires_in. The refresh token itself usually stays the same.
Why does my YouTube refresh token stop working?
Google invalidates refresh tokens in a handful of documented cases. Its OAuth 2.0 overview lists them:
- Testing status. An External app in Testing gets refresh tokens that expire in 7 days.
- Six months unused. A refresh token that hasn't been used for six months stops working.
- Too many tokens. Google allows 100 refresh tokens per Google account per OAuth client ID. Creating a 101st silently invalidates the oldest.
- The user revoked access, or chose time-based access that has run out.
- The user changed their password, when the token carries Gmail scopes. YouTube-only tokens aren't on that list.
A dead refresh token returns invalid_grant from the token endpoint. Mark the channel as disconnected and send the user back through Step 4. Retrying won't help.
Does a YouTube OAuth app need Google verification?
Yes, once it leaves Testing with YouTube scopes. Google's sensitive scope verification page says sensitive scopes "require review by Google before any Google Account can grant access," and gives deleting a YouTube video as an example. No YouTube Data API scope appears on Google's list of restricted scopes, which names only the Data Portability API's YouTube scopes, so the extra security assessment for restricted scopes doesn't apply. The Data Access page in Google Cloud shows which category each scope you add falls into.
What the review asks for:
- A demo video, uploaded to YouTube as unlisted, showing a user granting each scope and your app using it.
- A justification for each scope, including why a narrower one won't work.
- A privacy policy on the same domain as your home page, linked from the consent screen.
- Domain verification in Google Search Console.
Google says the review typically takes 3 to 5 business days. Until it passes, users see an "unverified app" screen, and Google's unverified apps page caps you at 100 new users.
OAuth verification is not the YouTube API Services audit. They're separate reviews. The audit checks your app against YouTube's API terms, and you need it to raise the default upload quota and to stop uploads being locked to private. Google covers it on its quota and compliance audits page, and our review-times post covers how long it takes.
Is there a faster way to handle YouTube OAuth?
Yes: let an API that already owns the Google client run the flow. Adeli's YouTube API holds the Google OAuth client, the client secret, and the callback, and its YouTube app has passed the YouTube API Services audit. You don't create a Google Cloud project, a consent screen, or a refresh job.
Your server starts a hosted connection with one request to POST /api/v1/profiles/{profileId}/connect:
curl -X POST "https://app.tryadeli.com/api/v1/profiles/$PROFILE_ID/connect" \
-H "Authorization: Bearer $ADELI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"platform":"youtube","redirectUrl":"https://client.example/oauth/adeli"}'The 201 response carries an authUrl. Open it in your customer's browser, and Google's account chooser lets them pick their own channel or a Brand Account channel. Adeli then sends the browser back to your registered redirectUrl, and your server polls the connection session until it's connected.
What Adeli handles from there, per its connect docs:
- Scopes. Adeli requests
youtube.force-ssl, which covers uploads, comments, and moderation, plusyt-analytics.readonlyfor analytics. - Consent failures. A customer who unticks the YouTube permission gets
missing_scope. An account with no channel getsno_channel. Nothing is stored in either case. - Token refresh. Adeli refreshes tokens for every connected channel.
POST /api/v1/profiles/{profileId}/accounts/refreshforces a refresh and also re-reads the channel's title, avatar, and counts. - Revoked grants. If a customer removes Adeli from their Google account, or the grant goes six months unused, the account shows
reconnect_requiredand you send them through connect again. Reconnecting the same channel keeps the sameaccountId.
Once a channel is connected, the same posting API uploads to YouTube and publishes to Instagram, TikTok, Facebook, X, and Bluesky. A YouTube channel is one connected account, and your first 3 connected accounts are free.