The YouTube Data API, explained
The YouTube Data API v3 is Google's REST API for YouTube data: videos, channels, playlists, search, comments, captions, and subscriptions, all under /youtube/v3/ on www.googleapis.com. Reading public data needs only an API key from a Google Cloud project with the API enabled. Anything private to a user, and every insert, update, or delete, needs OAuth 2.0. Each project gets a default daily quota of 10,000 units for most methods, where a read usually costs 1 unit and a write 50, plus separate allowances of 100 search.list calls and 100 videos.insert calls.
What the API covers
The API is organized around resources, and most resources support some of four
operations: list (GET), insert (POST), update
(PUT), and delete (DELETE) (YouTube Data API
overview). The ones most apps use:
| Resource | What it is |
|---|---|
videos | A video's metadata, statistics, and status; uploads go through videos.insert |
channels | A channel's details; forHandle looks a channel up by its @handle |
search | Videos, channels, or playlists matching a query; results point to other resources |
playlists, playlistItems | Playlists and the videos in them |
commentThreads, comments | Top-level comments with their replies, and single comments |
captions, thumbnails | Caption tracks and custom thumbnails for a video |
subscriptions | A user's channel subscriptions |
Every request that returns a resource must say which parts to return,
such as snippet, statistics, or status for a
video. The optional fields parameter trims the response further, down to
single nested properties. Asking only for what you use keeps responses small.
How to get a YouTube API key
- Create or select a project in the Google Cloud console.
- Open the API Library and enable YouTube Data API v3.
- On the Credentials page, choose Create credentials > API key.
- Choose Restrict key and add application and API restrictions before you use it in production.
The key identifies your project, and its requests count against that project's quota (implement authorization). Keep it on your server: a key shipped in a web page or app can be copied and its quota spent by someone else.
API key or OAuth 2.0?
Google's rule is short. A request that doesn't carry an OAuth 2.0 token must send an API key, and a key can only retrieve public data. Inserts, updates, deletes, and reads of private data need OAuth 2.0 (implement authorization).
| You want to | Credential |
|---|---|
| Read public videos, channels, playlists, search results, or comments | API key |
Read the signed-in user's own channel, e.g. channels.list with mine=true | OAuth 2.0 |
| Upload, edit, or delete videos and playlists; post or moderate comments | OAuth 2.0 |
For OAuth, create an OAuth client ID on the same Credentials page, send the user to
Google's consent screen with the scopes you need, and exchange the returned code for an
access token and a refresh token. Send the access token as
Authorization: Bearer <ACCESS_TOKEN>. The common YouTube scopes,
all under https://www.googleapis.com/auth/, are
youtube.readonly ("View your YouTube account"),
youtube.upload ("Manage your YouTube videos"),
youtube ("Manage your YouTube account"), and
youtube.force-ssl ("See, edit, and permanently delete your YouTube videos,
ratings, comments, and captions"). Each method's reference page lists the scopes it
accepts; ask for the narrowest one that covers your calls; the
YouTube upload API guide covers the upload
scope, offline access, and the 7-day refresh-token limit for apps still in Testing.
Quota: what each call costs
The API limits use with quota units, not request counts. By default a project gets
100 search.list calls, 100 videos.insert calls, and 10,000
units a day combined for the other methods, apart from videos.batchGetStats,
which has its own 10,000-unit bucket. Daily quotas reset at midnight Pacific Time, and every request costs at least 1 unit, including invalid ones
(quota costs). Common costs:
| Call | Cost |
|---|---|
videos.list, channels.list, playlists.list, playlistItems.list, commentThreads.list, comments.list, subscriptions.list | 1 unit |
search.list | 1 of its own 100 calls a day |
videos.insert | 1 of its own 100 calls a day |
videos.update, videos.rate, videos.delete, playlists.insert, playlistItems.insert, commentThreads.insert, comments.insert, thumbnails.set | 50 units |
captions.list | 50 units |
captions.insert, captions.update | 400 and 450 units |
Paging costs quota too: each extra page of results is a new request at the full cost.
With 10,000 units you can make about 10,000 list calls a day, or about 200 writes at 50
units each, but only 100 searches however much of the main bucket is left. When a
playlist or a known set of video IDs can answer the question, read it with
playlistItems.list or videos.list instead of searching. Your
current usage is on the Quotas page in the Cloud console, and you can ask for more
through the quota extension request form.
Your first request
A videos.list call with an API key, asking for two parts and trimming the
response with fields. It costs 1 unit:
curl "https://www.googleapis.com/youtube/v3/videos?part=snippet,statistics&id=VIDEO_ID&fields=items(id,snippet/title,statistics/viewCount)&key=YOUR_API_KEY"
# 200 OK
# {"items": [{"id": "VIDEO_ID", "snippet": {"title": "..."}, "statistics": {"viewCount": "..."}}]}
Pass several video IDs, comma-separated, in id to read them in one call
for the same 1 unit.
Pagination
List methods take maxResults and return nextPageToken (and
sometimes prevPageToken) when there is more; send the token back as
pageToken to get the next page (pagination).
The page size depends on the method. search.list and
channels.list take 0 to 50 and default to 5
(search.list, channels.list);
commentThreads.list takes 1 to 100 and defaults to 20
(commentThreads.list). Set it explicitly: with the
default you get at most 5 search results a page and pay for the next page sooner than
you need to.
Calling the API from TypeScript
You don't need a client library for reads. This function reads public video
statistics with an API key and plain fetch. It treats the response as
untrusted, reports only error reasons it recognizes, and fails on a response without an
items array instead of returning an empty list:
// Public data only: an API key is enough. Restrict the key in the Cloud console.
const apiKey = process.env["YOUTUBE_API_KEY"];
if (!apiKey) throw new Error("Set YOUTUBE_API_KEY");
type VideoStats = { id: string; title: string; views: string | undefined };
const knownReasons = new Set(["quotaExceeded", "forbidden", "videoNotFound"]);
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
// Report only reasons we recognize, so nothing else from the response reaches logs.
function errorReason(body: unknown): string {
if (!isRecord(body) || !isRecord(body["error"])) return "unknown";
const errors = body["error"]["errors"];
const first: unknown = Array.isArray(errors) ? errors[0] : undefined;
const reason = isRecord(first) ? first["reason"] : undefined;
return typeof reason === "string" && knownReasons.has(reason) ? reason : "unknown";
}
function toVideoStats(item: unknown): VideoStats[] {
if (!isRecord(item) || typeof item["id"] !== "string" || !isRecord(item["snippet"])) return [];
const title = item["snippet"]["title"];
const stats = item["statistics"];
const views = isRecord(stats) && typeof stats["viewCount"] === "string" ? stats["viewCount"] : undefined;
return typeof title === "string" ? [{ id: item["id"], title, views }] : [];
}
export async function videoStats({ apiKey, ids }: { apiKey: string; ids: readonly string[] }): Promise<VideoStats[]> {
const url = new URL("https://www.googleapis.com/youtube/v3/videos");
url.searchParams.set("part", "snippet,statistics"); // videos.list: 1 unit per call
url.searchParams.set("id", ids.join(","));
url.searchParams.set("fields", "items(id,snippet/title,statistics/viewCount)");
url.searchParams.set("key", apiKey);
const response = await fetch(url);
const body: unknown = await response.json().catch(() => undefined);
// quotaExceeded (403) lasts until midnight Pacific Time; don't retry it.
if (!response.ok) throw new Error(`videos.list failed: ${response.status} ${errorReason(body)}`);
// A 200 without an items array is not "no videos": fail instead of returning [].
if (!isRecord(body) || !Array.isArray(body["items"])) throw new Error("videos.list returned an unexpected response");
return body["items"].flatMap(toVideoStats);
}
console.log(await videoStats({ apiKey, ids: ["dQw4w9WgXcQ"] }));
We type-checked this example with strict TypeScript and ran it on 9 November 2026 with
Node.js 24 and Bun against a stub fetch: it returned the parsed item for a
normal response and an empty list for an empty items array, threw
videos.list failed: 403 quotaExceeded for a quota error and
502 unknown for an HTML error page, kept an unrecognized reason out of the
error, threw on a 200 with an HTML or error body, and skipped malformed
items. Google's own googleapis package for Node.js covers every method if
you'd rather use a generated client.
Errors you'll see
Errors come back with an HTTP status and a reason in error.errors[].reason
(errors):
| Reason | What it means |
|---|---|
quotaExceeded (403) | The project spent its quota for the day. Retrying won't help until midnight Pacific Time. |
forbidden (403) | Access forbidden: the request may not be properly authorized. Check that the token has a scope that covers the call. |
commentsDisabled (403) | From commentThreads.list: the video has comments turned off. |
channelNotFound (403) | The channel in the request can't be found. |
What changed in 2026
Older tutorials miss these (revision history):
- 1 June:
search.listandvideos.insertmoved to their own quota buckets, the start of a more granular quota system. Since 2014 a search had cost 100 units of the shared 10,000. - 3 June: new
videos.batchGetStatsmethod for video statistics, with its own 10,000-unit daily bucket. - 1 September:
videos.getRatingaccepts theyoutube.readonlyscope. - 14 September: custom thumbnails can be up to 50 MB, up from 2 MB.
- 2 November: videos uploaded with
videos.insertfrom unverified API projects are no longer locked to private, reversing the 2020 policy. - 3 November:
thumbnails.setcan set custom thumbnails for Shorts once processing is complete.
Calling the YouTube Data API with Social SDK
Social SDK is an open-source TypeScript library for posting to and
reading from social platforms. Its YouTube adapter takes an OAuth access token and a
channel ID, and besides uploading through posts.publish it exposes the
Data API's other calls as typed native operations: search,
playlists, playlistItems, captions,
subscriptions, updateVideo, setThumbnail,
comment moderation, ratings, and live broadcasts. This example searches one channel's
videos for a phrase, newest first, and counts the signed-in user's playlists:
npm install @opencoredev/social-sdk
import { createSocial } from "@opencoredev/social-sdk";
import { youtube } from "@opencoredev/social-sdk/youtube";
const accessToken = process.env["YOUTUBE_ACCESS_TOKEN"]; // OAuth token with the youtube.readonly scope
const channelId = process.env["YOUTUBE_CHANNEL_ID"];
if (!accessToken || !channelId) throw new Error("Set YOUTUBE_ACCESS_TOKEN and YOUTUBE_CHANNEL_ID");
const social = createSocial({ backend: youtube({ auth: { accessToken, channelId } }) });
// Native calls bypass client middleware: check your own user's access to this channel first.
const native = social.native("default", { acknowledgeUnsafe: true });
if (!native) throw new Error("YouTube native operations are unavailable");
const context = () => ({
backendInstance: "default",
correlationId: crypto.randomUUID(),
retryBudget: { maxAttempts: 1, maxElapsedMs: 30_000 },
});
// search.list: 1 call from the 100-a-day search bucket. Finds this channel's videos
// that match the phrase, newest first; it is not a list of every upload.
const matches = await native.search({ q: "release notes", channelId, type: "video", order: "date", maxResults: 10, context: context() });
// playlists.list: 1 unit from the shared 10,000-unit bucket.
const playlists = await native.playlists({ action: "list", mine: true, maxResults: 50, context: context() });
// Playlists can be private: log counts, not their contents.
const count = (page: Record<string, unknown>) => (Array.isArray(page["items"]) ? page["items"].length : 0);
console.log(`${count(matches)} matching videos, ${count(playlists)} playlists`);
We type-checked this example against @opencoredev/social-sdk 0.5.0 from
npm and ran it on 9 November 2026 with Node.js 24 and Bun against a stub
fetch. It sent two requests, both with the bearer token:
GET /youtube/v3/search with part=snippet,
type=video, the channel ID, order=date and
maxResults=10, then GET /youtube/v3/playlists with
mine=true and maxResults=50, and printed only the two counts,
since playlists can be private. That run cost 1 search call and 1 unit. A search
returns only videos that match the query; to list every upload, read the playlist
named in the channel's contentDetails.relatedPlaylists.uploads with
playlistItems instead. The adapter doesn't take API keys or refresh tokens; the SDK's
youtubeOAuth server helper handles Google's authorization code flow
(authentication guide). Native operations skip the
client's middleware, so check in your own server that the user may act on that channel
before you call one.
Every native operation, with examples, is on the YouTube platform page. Uploads, resumable sessions, and processing states are in the YouTube upload API guide. To build without a Google project, start with the mock quickstart, and the social media APIs guide compares YouTube with the other platforms.
Common questions
- How many requests can I make with the YouTube Data API per day?
-
By default, 10,000 quota units a day for most methods, plus 100
search.listcalls and 100videos.insertcalls in their own buckets. Most reads cost 1 unit and most writes 50, so the number of requests depends on which methods you call. - Do I need OAuth to read public YouTube data?
- No. An API key is enough for public videos, channels, playlists, search results, and comments. You need OAuth 2.0 for private data and for anything that changes data.
- How do I get a channel ID from a handle?
-
Call
channels.listwithforHandleset to the handle, with or without the @. The channel resource in the response has theid. - When does the YouTube API quota reset?
- At midnight Pacific Time, every day.
- How much quota does a search cost?
-
Since 1 June 2026, one call from
search.list's own bucket of 100 calls a day. It no longer comes out of the 10,000-unit bucket. Each extra page of results is another call. - How do I get more YouTube API quota?
- Fill in Google's quota extension request form for YouTube API Services. Check your current usage on the Quotas page in the Cloud console first.
Sources
Google's YouTube Data API pages, read on 9 November 2026; each showed "Last updated 3 November 2026":
- YouTube Data API overview: resources, operations, default quota,
partandfields - Implement authorization: API keys vs OAuth 2.0, project setup, key restrictions
- Quota costs: per-method costs, buckets, reset time, paging cost
- Pagination:
maxResults,pageToken - search.list, channels.list, commentThreads.list: parameters and limits
- Errors: core and per-method error reasons
- Revision history: the 2026 changes listed above
Rather not run it yourself? Social SDK Cloud, the hosted version, is taking early-access signups. Join the list.