updated 10 November 2026

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:

APIWhat it gives you
YouTube Analytics APIReports for a date range on channels you own: views, watch time, audience, traffic sources, revenue. Built for targeted queries.
YouTube Reporting APIBulk daily reports for the same kinds of data, which you schedule and download. Built for exports and warehouses.
YouTube Data APICurrent 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/:

ScopeWhat it unlocks
yt-analytics.readonlyUser activity metrics, like views and ratings
yt-analytics-monetary.readonlyThe same, plus estimated revenue and ad performance metrics
youtube.readonlyRequired too: reports.query requests must have access to this scope
youtubeManaging 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):

ParameterWhat it does
idsRequired. channel==MINE, channel==CHANNEL_ID, or contentOwner==OWNER_NAME
startDate, endDateRequired. YYYY-MM-DD; the response stops at the last day for which every requested metric is processed
metricsRequired. Comma-separated, such as views,estimatedMinutesWatched
dimensionsHow rows are grouped, such as day, country, or video
filtersWhich data to include, such as video==VIDEO_ID;country==US; join filters with ;
sort, maxResults, startIndexOrder (- for descending), row limit, and a 1-based offset for paging
currencyISO 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:

ReportDimensions and rules
Channel totals for the periodNo dimensions; filter by video, group, or a country
Views over timeExactly one of day or month
Top videosvideo, with sort required and maxResults of 200 or less
Traffic sources, playback locationsinsightTrafficSourceType, insightPlaybackLocationType; the detail versions allow 25 rows at most and need sort
Geographycountry, or province with country==US
DevicesdeviceType, operatingSystem
DemographicsageGroup, gender, with the viewerPercentage metric
Audience retentionelapsedVideoTimeRatio, 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. engagedViews keeps 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.list and videos.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 (or yt-analytics-monetary.readonly) and access to youtube.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.viewCount from videos.list.
How do I get revenue from the YouTube Analytics API?
Authorize with yt-analytics-monetary.readonly and request metrics such as estimatedRevenue. The channel must be in the YouTube Partner Program; otherwise the request returns 403.
How do I get my top videos?
Use dimensions=video with sort=-views (or another supported metric) and maxResults of 200 or less. Both sort and maxResults are 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.readonly requirement, 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.