The YouTube Analytics API, explained
The YouTube Analytics API returns a channel's analytics as a table: views, watch time, likes, subscribers gained, and, for monetized channels, estimated revenue, grouped by dimensions such as day, country, or video. Every report comes from one method, reports.query, a GET request to /v2/reports on youtubeanalytics.googleapis.com, and every request needs an OAuth 2.0 token from the channel's owner; API keys don't work. Data typically arrives 48 to 72 hours late, and each request counts as one unit of quota.
Analytics API or Data API?
YouTube has three APIs that return numbers, and they answer different questions:
| API | What it gives you |
|---|---|
| YouTube Analytics API | Reports for a date range on channels you own: views, watch time, audience, traffic sources, revenue. Built for targeted queries. |
| YouTube Reporting API | Bulk daily reports for the same kinds of data, which you schedule and download. Built for exports and warehouses. |
| YouTube Data API | Current public counters on any video or channel, such as viewCount, through videos.list and channels.list. |
Google's data model page says Analytics data is not real-time and points apps that
need current view, like, or comment counts to the Data API's videos.list
instead (data model). The
YouTube Data API guide covers that side: API
keys, quota costs, and reading public statistics.
Authorization and scopes
Every request must carry an OAuth 2.0 token. For a channel report, set
ids to channel==MINE for the signed-in user's channel, or
channel==CHANNEL_ID, in which case the user who authorized the request must
own that channel (channel reports). You can't read
another creator's analytics, only your own users' channels. The scopes, all under
https://www.googleapis.com/auth/:
| Scope | What it unlocks |
|---|---|
yt-analytics.readonly | User activity metrics, like views and ratings |
yt-analytics-monetary.readonly | The same, plus estimated revenue and ad performance metrics |
youtube.readonly | Required too: reports.query requests must have access to this scope |
youtube | Managing Analytics groups and group items as a channel owner |
The youtube.readonly requirement trips people up because the analytics
scopes look sufficient. Google's reports.query reference states it at the
top, and the revision history dates it to 18 June 2018
(reports.query, revision
history). Request it alongside yt-analytics.readonly.
Revenue needs more than the monetary scope: the channel must be in the YouTube Partner
Program, and asking for monetary metrics on a channel that isn't returns
403 Forbidden (channel reports). Content
owner reports, which add up every channel linked to a content owner, are only for YouTube
Partner Program content partners.
The reports.query request
Four parameters are required, and the rest shape the table (reports.query):
| Parameter | What it does |
|---|---|
ids | Required. channel==MINE, channel==CHANNEL_ID, or contentOwner==OWNER_NAME |
startDate, endDate | Required. YYYY-MM-DD; the response stops at the last day for which every requested metric is processed |
metrics | Required. Comma-separated, such as views,estimatedMinutesWatched |
dimensions | How rows are grouped, such as day, country, or video |
filters | Which data to include, such as video==VIDEO_ID;country==US; join filters with ; |
sort, maxResults, startIndex | Order (- for descending), row limit, and a 1-based offset for paging |
currency | ISO 4217 code for revenue metrics; the default is USD |
The video, playlist, and channel filters accept
a comma-separated list of up to 500 IDs, and adding the same name to
dimensions splits the results per ID. A group, a saved collection of up to
500 videos, playlists, channels, or assets, can stand in for a long ID list.
Your first request
Daily views and minutes watched for October, for the signed-in user's channel. It costs 1 unit of quota:
curl -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://youtubeanalytics.googleapis.com/v2/reports?ids=channel==MINE&startDate=2026-10-01&endDate=2026-10-31&metrics=views,estimatedMinutesWatched&dimensions=day&sort=day"
# 200 OK
# {"kind": "youtubeAnalytics#resultTable",
# "columnHeaders": [{"name": "day", "columnType": "DIMENSION", "dataType": "STRING"},
# {"name": "views", "columnType": "METRIC", "dataType": "INTEGER"}, ...],
# "rows": [["2026-10-01", 120, 340], ["2026-10-02", 80, 200], ...]}
The response is a table. columnHeaders lists the dimensions you asked for,
then the metrics, each with a columnType of DIMENSION or
METRIC; each entry in rows is one array of values in that
order. Google's advice is to read column positions from the header row rather than
assume them, and if there is no data for the period, rows is left out
entirely (data model, reports.query).
Reports you can build
There is no list of report endpoints: a report is a combination of dimensions, metrics, and filters that Google's channel reports page allows. The common ones:
| Report | Dimensions and rules |
|---|---|
| Channel totals for the period | No dimensions; filter by video, group, or a country |
| Views over time | Exactly one of day or month |
| Top videos | video, with sort required and maxResults of 200 or less |
| Traffic sources, playback locations | insightTrafficSourceType, insightPlaybackLocationType; the detail versions allow 25 rows at most and need sort |
| Geography | country, or province with country==US |
| Devices | deviceType, operatingSystem |
| Demographics | ageGroup, gender, with the viewerPercentage metric |
| Audience retention | elapsedVideoTimeRatio, filtered to a single video |
A combination the page doesn't allow fails with 400 badRequest and the
message "The query is not supported." Metric names are camelCase, such as
views, engagedViews, estimatedMinutesWatched,
averageViewDuration, likes, shares,
subscribersGained, and estimatedRevenue
(metrics, dimensions).
Why the numbers lag and don't add up
- 48 to 72 hours late. Processing typically takes that long, so a report never covers the last few days, and its end date is cut back to the last fully processed day. Numbers can differ from YouTube Studio's real-time view.
- Small numbers are withheld. When a video, a country, a traffic source, or a demographic falls under an unpublished threshold, its rows are left out. A video's total can be 1,000 views while its by-country report only shows 998.
- Deleted videos stay in totals. Channel-wide totals include views of videos later deleted, but per-video reports leave those videos out, so the per-video rows can add up to less.
- Views changed meaning in 2026. Since 27 August, public view counts start when a video begins to play, across long-form, Live, and Shorts.
engagedViewskeeps the older "past the first frame" sense and is what monetization uses.
Sources: the data model page for the first three and the revision history for the last.
Quota
Each Analytics API request counts as one unit of your project's quota, whatever the report. Your limits are on the Quotas panel in the Google Cloud console (data model). Asking for several metrics and dimensions in one request costs the same as asking for one, so fetch a whole dashboard's numbers in as few requests as the report rules allow.
When to use the Reporting API instead
The YouTube Reporting API doesn't answer queries. You create a reporting job for a
report type, YouTube generates a report for each day, and you download it later, then
filter and sort it yourself. It has data the Analytics API doesn't, such as subtitle
and asset reports and some revenue data, and its field names use
snake_case where the Analytics API uses camelCase
(which API is right for you). Thumbnail impressions and
click-through rate arrived in the Reporting API's reach reports on 15 January 2026
(revision history). Use the Analytics API for numbers a
user asks for on screen, and the Reporting API to keep a full daily history in your own
database.
Calling the API from TypeScript
No client library is needed. This function reads daily views and watch time with
fetch. It finds columns by name from the header row, treats a missing
rows as an empty period, reports only error reasons it recognizes, and
fails on a response that isn't a result table:
// Channel analytics need an OAuth token for the channel's owner, with the
// yt-analytics.readonly and youtube.readonly scopes. API keys don't work here.
const accessToken = process.env["YOUTUBE_ACCESS_TOKEN"];
if (!accessToken) throw new Error("Set YOUTUBE_ACCESS_TOKEN");
type DailyViews = { day: string; views: number; minutesWatched: number };
const knownReasons = new Set(["forbidden", "insufficientPermissions", "badRequest", "quotaExceeded", "invalid"]);
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";
}
export async function dailyViews({ token, startDate, endDate }: { token: string; startDate: string; endDate: string }): Promise<DailyViews[]> {
const url = new URL("https://youtubeanalytics.googleapis.com/v2/reports");
url.searchParams.set("ids", "channel==MINE");
url.searchParams.set("startDate", startDate);
url.searchParams.set("endDate", endDate);
url.searchParams.set("metrics", "views,estimatedMinutesWatched");
url.searchParams.set("dimensions", "day");
url.searchParams.set("sort", "day");
const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
const body: unknown = await response.json().catch(() => undefined);
if (!response.ok) throw new Error(`reports.query failed: ${response.status} ${errorReason(body)}`);
if (!isRecord(body) || !Array.isArray(body["columnHeaders"])) throw new Error("reports.query returned an unexpected response");
// Find columns by name from the header row; never assume their order.
const names = body["columnHeaders"].map((header: unknown) => (isRecord(header) ? header["name"] : undefined));
const day = names.indexOf("day");
const views = names.indexOf("views");
const minutes = names.indexOf("estimatedMinutesWatched");
if (day < 0 || views < 0 || minutes < 0) throw new Error("reports.query returned unexpected columns");
// No rows at all means no data for the period, not an error.
const rows: unknown = body["rows"] === undefined ? [] : body["rows"];
if (!Array.isArray(rows)) throw new Error("reports.query returned unexpected rows");
return rows.flatMap((row: unknown) => {
if (!Array.isArray(row)) return [];
const [d, v, m]: unknown[] = [row[day], row[views], row[minutes]];
return typeof d === "string" && typeof v === "number" && typeof m === "number" ? [{ day: d, views: v, minutesWatched: m }] : [];
});
}
const days = await dailyViews({ token: accessToken, startDate: "2026-10-01", endDate: "2026-10-31" });
// Channel analytics are private to the owner: log counts, not their values.
console.log(`${days.length} days of data`);
We type-checked this example with strict TypeScript and ran it on 10 November 2026 with
Node.js 24 and Bun against a stub fetch. It sent one request with the
bearer token, read rows correctly when the stub put the columns in a different order,
returned an empty list when rows was missing and threw when it was null, skipped a malformed row,
threw reports.query failed: 403 forbidden for a permission error and
502 unknown for an HTML error page, kept an unrecognized reason and the
error message out of the thrown error, and threw on a 200 with an HTML
body, an error body, or a rows value that wasn't an array. The
googleapis package for Node.js has a generated client if you'd rather use
one.
YouTube analytics 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. social.analytics.getReport calls reports.query
for that channel with a date range, metrics, and dimensions, and returns rows of named
dimensions and numeric metrics, with rows: [] for an empty period. It
doesn't send sort, maxResults, or filters, so a
top-videos report goes through the adapter's native analytics call, which
passes your query parameters as they are:
npm install @opencoredev/social-sdk
import { connectedAccountRef, createSocial } from "@opencoredev/social-sdk";
import { youtube } from "@opencoredev/social-sdk/youtube";
// OAuth token for the channel's owner, with yt-analytics.readonly and youtube.readonly.
const accessToken = process.env["YOUTUBE_ACCESS_TOKEN"];
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 } }) });
const account = connectedAccountRef({ backend: "default", platform: "youtube", accountId: channelId });
// Daily views and watch time: one reports.query call.
const report = await social.analytics.getReport(account, {
from: "2026-10-01",
to: "2026-10-31",
metrics: ["views", "estimatedMinutesWatched"],
dimensions: ["day"],
});
// Top videos need sort and maxResults, which getReport doesn't send, so use the
// native call. Native calls bypass client middleware: check your user's access first.
const native = social.native("default", { acknowledgeUnsafe: true });
if (!native) throw new Error("YouTube native operations are unavailable");
const top = await native.analytics({
query: {
ids: `channel==${channelId}`,
startDate: "2026-10-01",
endDate: "2026-10-31",
metrics: "views,estimatedMinutesWatched",
dimensions: "video",
sort: "-views",
maxResults: "10",
},
context: { backendInstance: "default", correlationId: crypto.randomUUID(), retryBudget: { maxAttempts: 1, maxElapsedMs: 30_000 } },
});
const topRows = Array.isArray(top["rows"]) ? top["rows"].length : 0;
// Channel analytics are private to the owner: log counts, not their values.
console.log(`${report.rows.length} days of data, ${topRows} top videos`);
We type-checked this example against @opencoredev/social-sdk 0.5.0 from
npm and ran it on 10 November 2026 with Node.js 24 and Bun against a stub
fetch. It sent two GET /v2/reports requests to
youtubeanalytics.googleapis.com, both with the bearer token and
ids=channel== the configured channel: one with
dimensions=day, and one with dimensions=video,
sort=-views and maxResults=10. It printed only row counts, since a
channel's analytics are private to its owner. The same two requests against Google
would cost 2 units of quota. A 403
surfaced as a missing_permission error and an HTML response as
upstream_failure, without the response body. The adapter also reads
lifetime counters without the Analytics API: getAccountMetrics returns the
channel's viewCount, subscriberCount and
videoCount, and getPostMetrics a video's views, likes, and
comments, both from the Data API. The adapter doesn't refresh tokens; the SDK's
youtubeOAuth server helper handles Google's authorization code flow
(authentication guide). Its default scopes cover
uploads and youtube.readonly but not analytics, and a scopes
option replaces the defaults, so pass the full list, including
yt-analytics.readonly and youtube.readonly. Native calls skip the
client's middleware, so check in your own server that the user may read that channel
before you make one.
The normalized report shape and every platform's analytics support are on the analytics reports page, and the YouTube adapter's other operations are on the YouTube platform page. Uploading is covered in the YouTube upload API guide. To try the SDK without a Google project, start with the mock quickstart.
Common questions
- Is the YouTube Analytics API free?
- Google's pages list no price for it. Usage is limited by quota instead: each request counts as one unit against your Google Cloud project's limits.
- Can I get analytics for a channel I don't own?
-
No. The user who authorizes the request must own the channel, or be the content
owner it's linked to. For another channel's public view, subscriber, and video
counts, use the Data API's
channels.listandvideos.list. - Can I use an API key with the YouTube Analytics API?
-
No. Every request needs an OAuth 2.0 token with
yt-analytics.readonly(oryt-analytics-monetary.readonly) and access toyoutube.readonly. - Why doesn't the API show yesterday's views?
-
Processing typically takes 48 to 72 hours, and the response stops at the last day for
which all requested metrics are ready. For current counts, read
statistics.viewCountfromvideos.list. - How do I get revenue from the YouTube Analytics API?
-
Authorize with
yt-analytics-monetary.readonlyand request metrics such asestimatedRevenue. The channel must be in the YouTube Partner Program; otherwise the request returns 403. - How do I get my top videos?
-
Use
dimensions=videowithsort=-views(or another supported metric) andmaxResultsof 200 or less. BothsortandmaxResultsare required for this report.
Sources
Google's YouTube Analytics and Reporting API pages, read on 10 November 2026:
- Analytics and Reporting APIs overview (last updated 3 November 2026): targeted queries vs bulk reports, groups
- reports.query (last updated 15 October 2026): parameters, scopes, the
youtube.readonlyrequirement, response format - Channel reports (last updated 15 October 2026):
ids, ownership, monetary access, report tables, row limits - Metrics (last updated 14 September 2026) and dimensions (last updated 9 November 2026)
- Data model (last updated 5 November 2026): data delay, header rows, deleted items, anonymization, quota
- Which API is right for you (last updated 5 November 2026): Analytics API vs Reporting API
- Revision history (last updated 9 November 2026): the 2018 scope change and the 2026 changes above
Rather not run it yourself? Social SDK Cloud, the hosted version, is taking early-access signups. Join the list.
Related guides
- The YouTube Data API, explained
- Facebook Page access tokens, explained
- Ayrshare alternatives for developers
- The YouTube upload API, explained
- The LinkedIn Posts API, explained
- Social media APIs for developers
- The Buffer API, explained
- X API pricing, explained
- The Bluesky API, explained
- The Threads API, explained