Fetch YouTube subscribers and comments with Apps Script

today February 9, 2023

YouTube has a terrific user interface that provides channel managers and owners like you and me with valuable statistics about the performance of our channels and their videos. But YouTube also has rich APIs that enable us to pull the data out of the system to anywhere we to consume it. So lets build a Google Apps Script webapp that will respond to GET requests and send over subscriber and comment information.

Interested in customizing this script? Contact me

To set up the system, create a brand new Google Apps Script file. Separately, create an HTML file that will call the webapp. Note that the HTML file is a standalone, and not part of the Apps Script project.

To pull both subscriber and comment information, we need to enable two advanced services that Google Apps Script offers: YouTube Analytics API and YouTube Data API. Open Apps Script, click on the plus icon next to Services and add the two aforementioned APIs one by one. Be careful not to change the Identifier for either of them: they should retain their default names of YouTubeAnalytics and YouTube.

Our task list for this project:

  • Declare our channel ID
  • Respond to get requests from the HTML page
  • Get the number of YouTube subscribers
  • Get the number of un-replied comments across all videos
  • Publish Apps Script as a web app
  • Build the HTML and Javascript to fetch data

Handle channel ID

Both advanced services are going to require the ID of the channel from which we are to pull the data. Your channel ID is available by clicking on your profile icon in YouTube, selecting Settings and then Advanced Settings. The ID will be listed on the page. Copy the ID. Then, in, add the following:

const g = {
  channelId: 'paste_your_channel_id_here'

Handle GET requests

Our HTML page will use vanilla Javascript to call our webapp. Let's create a function that Apps Script will execute automatically whenever the web app will receive a GET request. Add the following below global g declaration:

function doGet() {
  const payload = {};
  try {
    payload.subscribers = getSubscribers();
    payload.comments = getComments();
  } catch (err) {
    payload.err = err.message;
  return ContentService.createTextOutput(JSON.stringify(payload)).setMimeType(

In the doGet function above, we define a "payload" object that will contain our YouTube data. We then invoke try/catch in order to handle any unexpected errors in fetching the data. We get subscribers and comments using functions that we will define next. If there is an error then we attach it to the payload. Finally, we use "ContentService" to stringify the payload and send it back to the client.

I can't stress enough how important it is to use try/catch in this case. If you don't and the server error, you may receive unexpected CORS errors on the client-side. Those may be difficult to deal with and identify their root cause. The try/catch takes care of that issue.

Get YouTube subscribers

Our function to fetch the total number of subscribers is short and sweet:

function getSubscribers() {
  let startDate = formatDateString(new Date('01-01-2021'));
  let endDate = formatDateString(new Date());
  const resp = YouTubeAnalytics.Reports.query({
    ids: `channel==${g.channelId}`,
    metrics: 'subscribersGained',
  return resp.rows[0][0];

function formatDateString(date) {
  return Utilities.formatDate(date, Session.getScriptTimeZone(), 'yyyy-MM-dd');

Above we define a start date (I chose Jan 1, 2021; feel free to change it), and then we use formatDateString to, well format it to a date string as YouTube API expects us to. Same goes for endDate which is today.

Next, we use ur YouTubeAnalytics advanced service to query our data. We pass our channel ID, our dates, and the metric we're interested in: 'subscribersGained'. The data returns as a nested array named 'rows', so we get the first element of the first element.

Get Un-replied comments

I want to get the number of comments that currently don't have a reply. This time the process is more involved: I need to get the "uploads" playlist, extract all the videos from it, extract all comments of each video, and then filter the comments to those that don't have a reply:

function getYouTubeUnrepliedComments() {
  const channel = YouTube.Channels.list('contentDetails', { id: g.channelId });
  const playlistId = channel.items[0].contentDetails.relatedPlaylists.uploads;
  let nextPageToken;
  let videos = [];
  do {
    const playlistResponse = YouTube.PlaylistItems.list('snippet', {
      maxResults: 50,
      pageToken: nextPageToken,
    if (playlistResponse) {
      if (playlistResponse.items && playlistResponse.items.length > 0) {
    nextPageToken = playlistResponse.nextPageToken;
  } while (nextPageToken);

  const comments = videos.flatMap((video) => {
    const videoId = video.snippet.resourceId.videoId;
    const commentThreads = YouTube.CommentThreads.list('snippet', { videoId });
      (item) => item.snippet.topLevelComment.snippet
  const unrepliedComments = comments.filter(
    (comment) => comment.totalReplyCount == 0
  return unrepliedComments.length;

First, we get the channel, using our second advanced service, YouTube. Again, we must include our channel ID. We then get the id of our "Uploads" playlist. We need to get all the videos, which can be a large number, so we have to page through the videos. We create a loop where we ask for 50 videos at a time from our playlist ID. We pass a pageToken which initially is null, but then is attached to the playlistResponse as long as there are additional videos to pull.

We check that the response includes videos and push these videos to a "videos" array. Next, we iterate over the videos, get each video's ID and use it to get the comment threads of each video. We retrieve the comments and flatten them to a simple array.

Finally, we filter comments to those that have zero replies and return their count.

Deploy the webapp

Now that our server-side code is complete, we need to deploy it as a webapp so that we can get a URL for our GET requests. Click on the blue Deploy button and select "New deployment." Click on the gear icon and select "Web app". Select "Execute as me" and Who as access: Anyone. This is because we won't do any Google authentication in the HTML page. Click Deploy and copy the web app URL.

Set up the client HTML

You can obviously set up the client any way you want. Let's go with something super simple. In your standalone HTML page, add the following:

<!DOCTYPE html>
<html lang="en">
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>YouTube Stats</title>
      body {
        font-family: sans-serif, Arial;
      td {
        padding: 20px;
      .container {
        width: 90%;
        margin: auto;
    <div class="container">
      <h1>YouTube Stats</h1>
          <td>Total subscribers</td>
          <td id="total-subscribers"></td>
          <td>Unreplied comments</td>
          <td id="unreplied-comments"></td>

Above,we have a body with a header and a table with two cells that have IDs, which we will use to populate the server data. I added some minimal styling as well.

Inside the script tag at the bottom of the markup, let's add the Javascript that will fetch the data from the server:

async function getStats() {
  try {
    const webappUrl = 'paste_your_webapp_url_here';
    const headers = new Headers();
    headers.append('Content-Type', 'text/plain;charset=utf-8');
    const req = new Request(webappUrl, {
      redirect: 'follow',
    const res = await fetch(req);
    const stats = await res.json();
    if (stats.err) {
    document.querySelector('#unreplied-comments').innerText =
    document.querySelector('#total-subscribers').innerText =
  } catch (err) {
    console.log(`Failed with the error: ${err}`);


Above, we define an asynchronous function because it will includes a fetch call, which is an async operation. We want to "await" the async operations so we define the containing function as async. Paste your webapp URL that you copied when you deployed the Apps script into the webappUrl placeholder.

We create a request object that includes redirect: follow, because Apps Script serves the content from a different URL than our web app, so we need to follow the redirect. Once we get the data, we populate the two table cells. Finally, we invoke the function so that the table will be updated a few seconds after the page loads.

Happy YouTubing!

Interested in customizing this script? Contact me