Solution 1 :

I do not think the API has any functionality around chapters.

The way chapters work is that they are derived from the timestamps put in the video description. Therefore, although it is a workaround, if you truly wanted to get the chapter information of the video, you could parse the description in order to get the timestamps and their names.

I ran a quick Ctrl+F on the API page to see if the iFrame API gives you access to the description of videos, that doesn’t seem to be the case. I could be wrong though, in that case ignore the instructions below.

In order to get the description, if you have the video ID you can use the Youtube Data API (different api, so you’ll probably need another api key for it), specifically the list method from the Videos section. You can get the description by passing “snippet” as and argument for the “part” parameter.

Solution 2 :

I know the question a long time ago, but I will share my answer for public knowledge.

sample python code:

# API client library
import googleapiclient.discovery
import re
# API information

api_service_name = "youtube"
api_version = "v3"
# API key
API_KEY = "your_api_key"
# API client
youtube =
   api_service_name, api_version, developerKey = API_KEY)

#run getVideoTimelineById using videoId = CCRTu3AQQeY


{'timeline': [{'time': '0:00', 'label': ' Teaser'}, {'time': '0:34', 'label': ' #1 Stop using Libraries'}, {'time': '2:45', 'label': ' #2 Learn about Streams'}, {'time': '5:24', 'label': ' #3 Learn basic of Networking / Linux Fundamentals'}, {'time': '6:47', 'label': ' #4 Learn about Event-loop & Aync model'}, {'time': '8:35', 'label': ' #5 Learn to use Debugger & Error handling'}, {'time': '10:29', 'label': ' Conclusion'}, {'time': '10:58', 'label': ' Outro'}]}

def getVideoTimelineById (videoId):
  request = youtube.videos().list(part="id,snippet",
                              id = videoId
  response = request.execute()
  des = response['items'][0]['snippet']['description']
  pattern = re.compile(r"((?:(?:[01]?d|2[0-3]):)?(?:[0-5]?d):(?:[0-5]?d)) 

  # find all matches to groups
  timeline = []
  result = {}
  for match in pattern.finditer(des):
    timeline.append({"time":, "label":})
  result = {"timeline": timeline}
  return result

Solution 3 :

I created a code which gets YouTube chapters data with JavaScript, regex and Node:

const axios = require('axios').default; // You have to install axios (npm install axios) in order to use this code

// Function for filtering array elements which don't contain a timestamp
const notText = (array) => {
    const regexTimePattern = /d:d/;
    for (let i = 0; i < array.length; i++) {
        let result = regexTimePattern.exec(array[i]);
        if (result === null) {
            array[i] = "";

const main = async (videoId) => {
    const videoDataResponse = await axios.get(
        { headers: { 'Accept': 'application/json' } }
    const description = JSON.stringify([0].snippet.description); //DESCRIÇÃO DO VÍDEO

    // Find [number]:[number] pattern in description
    const numberNumberPattern = /d:d/gi;
    let descriptionLines = [], numberNumbeResult;
    while ((numberNumbeResult = numberNumberPattern.exec(description))) {

    let min = [], sec = [], hour = [], chapterTitle = [], chapterStartIndex, chapterEndIndex;

    // Verifies if last [number]:[number] correspondence is in the last description line
    if (description.indexOf("\n", descriptionLines[descriptionLines.length - 1]) === -1) { //é na última linha
        chapterEndIndex = description.length - 1;
    } else { // not in the last line
        chapterEndIndex = description.indexOf("\n", descriptionLines[descriptionLines.length - 1]);

    // Verifies if first [number]:[number] correspondence is in the first description line
    switch (descriptionLines[0]) {
        case 1: // it's in the first line ([number]:[number] pattern)
            chapterStartIndex = 1;
        case 2: // it's in the first line ([number][number]:[number] pattern)
            chapterStartIndex = 2;
        default: //it's not in the first line
            const auxiliarString = description.substring(descriptionLines[0] - 6);
            chapterStartIndex = auxiliarString.indexOf("\n") + descriptionLines[0] - 4;

    // get description part with timestamp and titles
    const chapters = description.substring(chapterStartIndex, chapterEndIndex);

    // separete lines
    const notFilteredLine = chapters.split("\n");

    // filter lines which don't have [number]:[number] pattern

    // filter empty lines
    const filteredLine = notFilteredLine.filter(Boolean);

    for (let i = 0; i < filteredLine.length; i++) {
        const notNumberOrColonRegex = /[^:d]/; // not number nor ":"
        const numberRegex = /d/; // number
        let numberResult = numberRegex.exec(filteredLine[i]);
        filteredLine[i] = filteredLine[i].substring(numberResult.index); // starts line in first number found
        let notNumberOrColonResult = notNumberOrColonRegex.exec(filteredLine[i]);
        let tempo = filteredLine[i].substring(0, notNumberOrColonResult.index); // timestamp end (not number nor ":")
        let tempoSeparado = tempo.split(":"); // split timestamps in each ":"
        switch (tempoSeparado.length) {
            case 2: // doesn't have hour
                hour[i] = 0;
                min[i] = Number(tempoSeparado[0]);
                sec[i] = Number(tempoSeparado[1]);
            case 3: // has hour
                hour[i] = Number(tempoSeparado[0]);
                min[i] = Number(tempoSeparado[1]);
                sec[i] = Number(tempoSeparado[2]);
        const numberOrLetterRegex = /[a-z0-9]/i; // number or letter 
        const auxiliarString = filteredLine[i].substring(notNumberOrColonResult.index); // auxiliar string starts when not number nor ":"
        let numberOrLetterResult = numberOrLetterRegex.exec(auxiliarString);

        // chapter title starts in first letter or number found in auxiliarString
        chapterTitle[i] = auxiliarString.substring(numberOrLetterResult.index);
    let chaptersData = [];
    for (let i = 0; i < chapterTitle.length; i++) {
        chaptersData[i] = {
            chapterTitle: chapterTitle[i],
            chapterTimestamp: [hour[i], min[i], sec[i]]
  // chaptersData contains the data we are looking for
  // chaptersData is an array of objects, in the format {chapterTitle: 'chapter title example', chapterTimestamp: [01, 25, 39]}
  // chapterTimestamp is in the format [hour, minute, second]

// calls main with the ID of the video of interest
main('videoId'); // For example: main('R3WDe7byUXo');

I created this code a year ago, when I was starting to learn JavaScript, so I probably made some things in a more complicated way than I needed to. Read the first and the last comments on the code to understand how to use it. If you have any doubts or suggestions, please let me know.

Link for GitHub repository

Problem :

After including this script of YouTube iframe API
One can put a youtube video in a container (e.g. “container_id”) and use such methods as “seekTo()” and “play()” on their page elements. (Anywhere outside the iframe)

var player = new YT.Player('container_id', {
    videoId: 'video_id'

Video object can be accessed like that YT.get("container_id"). One can scroll to 1 minute mark by doing this YT.get("container_id").seekTo(60) but I can’t seem to find a “Chapters” object in it. “Chapters” are sections of a video that are separated by timestamps like this.

enter image description here

I was wandering if there is a way to get them as an array or an object or something but can’t seem to find it in the YT.get("container_id") result object.

Generated iFrame does contain them as html tags but it has no metadata like “starts at” or “chapter title” and it’s not really accessible because of CORS shenanigans. Does YouTube iframe api even send chapter data if it exists? (Lot’s of videos don’t have them)


Comment posted by Glenn Carver

It would appear that you are correct. What algorithm does youtube itself use to generate those captions on the video you think? A regex? What do you think it’d be? I am going to mark this as an answer for now at least, but I also agree that this is the only way.