updated 9 November 2026

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:

ResourceWhat it is
videosA video's metadata, statistics, and status; uploads go through videos.insert
channelsA channel's details; forHandle looks a channel up by its @handle
searchVideos, channels, or playlists matching a query; results point to other resources
playlists, playlistItemsPlaylists and the videos in them
commentThreads, commentsTop-level comments with their replies, and single comments
captions, thumbnailsCaption tracks and custom thumbnails for a video
subscriptionsA 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

  1. Create or select a project in the Google Cloud console.
  2. Open the API Library and enable YouTube Data API v3.
  3. On the Credentials page, choose Create credentials > API key.
  4. 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 toCredential
Read public videos, channels, playlists, search results, or commentsAPI key
Read the signed-in user's own channel, e.g. channels.list with mine=trueOAuth 2.0
Upload, edit, or delete videos and playlists; post or moderate commentsOAuth 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:

CallCost
videos.list, channels.list, playlists.list, playlistItems.list, commentThreads.list, comments.list, subscriptions.list1 unit
search.list1 of its own 100 calls a day
videos.insert1 of its own 100 calls a day
videos.update, videos.rate, videos.delete, playlists.insert, playlistItems.insert, commentThreads.insert, comments.insert, thumbnails.set50 units
captions.list50 units
captions.insert, captions.update400 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):

ReasonWhat 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.list and videos.insert moved 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.batchGetStats method for video statistics, with its own 10,000-unit daily bucket.
  • 1 September: videos.getRating accepts the youtube.readonly scope.
  • 14 September: custom thumbnails can be up to 50 MB, up from 2 MB.
  • 2 November: videos uploaded with videos.insert from unverified API projects are no longer locked to private, reversing the 2020 policy.
  • 3 November: thumbnails.set can 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.list calls and 100 videos.insert calls 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.list with forHandle set to the handle, with or without the @. The channel resource in the response has the id.
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":

Rather not run it yourself? Social SDK Cloud, the hosted version, is taking early-access signups. Join the list.