YouTube playlist duration API — get playlist length programmatically

Updated 2026-09-22 · 5 min read

YouTube's API doesn't have a "playlist duration" field. You get the video IDs in the playlist, then the duration of each video, then you add them up. This guide shows both the short way (one request to our endpoint) and the full way (the YouTube Data API v3 yourself), with the quota costs so you can budget.

Option A — one request to our endpoint

GET https://playlistduration.com/api/v1/playlist/{PLAYLIST_ID}
{
  "id": "PLxxxx",
  "url": "https://playlistduration.com/playlist/PLxxxx",
  "title": "CS50x 2024",
  "channel": { "id": "UC…", "title": "CS50" },
  "privacy": "public",
  "itemCount": 26,
  "counted": 25,
  "unavailable": 1,
  "truncated": false,
  "totalSeconds": 112397,
  "averageSeconds": 4496,
  "longest": { "id": "…", "n": 9, "seconds": 8402 },
  "shortest": { "id": "…", "n": 25, "seconds": 95 },
  "atSpeed": { "1": 112397, "1.25": 89918, "1.5": 74931, "1.75": 64227, "2": 56199 },
  "videos": [ { "n": 1, "id": "…", "title": "…", "seconds": 6543, "publishedAt": "…" } ],
  "fetchedAt": "2026-09-22T10:00:00.000Z",
  "cache": "hit"
}

Full reference with curl, JavaScript and Python examples: /developers. Terms: free for reasonable use; please link back to the result page (url in every response) wherever you show the numbers. If you're going to make more than a few thousand requests a day, tell us first.

Option B — the YouTube Data API v3 yourself

You'll need an API key: Google Cloud Console → new project → enable YouTube Data API v3 → Credentials → API key. Restrict the key to that one API.

The three calls

Step Endpoint Cost
Playlist title, count, privacy playlists.list?part=snippet,status,contentDetails&id=PL… 1 unit
Video IDs, 50 per page playlistItems.list?part=contentDetails&playlistId=PL…&maxResults=50[&pageToken=…] 1 unit per page
Durations, 50 IDs per call videos.list?part=contentDetails&id=id1,id2,… 1 unit per call

Cost for N videos ≈ 1 + 2 × ⌈N / 50⌉ units. A 500-video playlist is 21 units; a 5,000-video one is 201. The default daily quota is 10,000 units, so one key can freshly measure roughly 500 large playlists a day — plenty if you cache results.

JavaScript (Node 18+ or a browser with the key kept server-side)

const KEY = process.env.YOUTUBE_API_KEY;
const API = "https://www.googleapis.com/youtube/v3";

async function get(resource, params) {
  const qs = new URLSearchParams({ ...params, key: KEY });
  const res = await fetch(`${API}/${resource}?${qs}`);
  if (!res.ok) throw new Error(`${resource}: HTTP ${res.status}`);
  return res.json();
}

// "PT1H2M3S" -> 3723 seconds
function isoToSeconds(iso) {
  const m = /^P(?:(\d+)D)?T?(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$/.exec(iso ?? "");
  if (!m) return 0;
  const [, d, h, min, s] = m.map(Number);
  return (d || 0) * 86400 + (h || 0) * 3600 + (min || 0) * 60 + (s || 0);
}

export async function playlistDuration(playlistId) {
  const ids = [];
  let pageToken;
  do {
    const page = await get("playlistItems", {
      part: "contentDetails", playlistId, maxResults: "50",
      ...(pageToken ? { pageToken } : {}),
      fields: "nextPageToken,items/contentDetails/videoId",
    });
    ids.push(...page.items.map((i) => i.contentDetails.videoId));
    pageToken = page.nextPageToken;
  } while (pageToken && ids.length < 5000);

  let total = 0, unavailable = 0;
  for (let i = 0; i < ids.length; i += 50) {
    const batch = ids.slice(i, i + 50);
    const data = await get("videos", {
      part: "contentDetails", id: batch.join(","),
      fields: "items(id,contentDetails/duration)",
    });
    const found = new Map(data.items.map((v) => [v.id, isoToSeconds(v.contentDetails.duration)]));
    for (const id of batch) {
      const s = found.get(id) ?? 0;
      if (s > 0) total += s; else unavailable++;
    }
  }
  return { videos: ids.length, unavailable, totalSeconds: total };
}

Python (requests)

import os, re, requests

KEY = os.environ["YOUTUBE_API_KEY"]
API = "https://www.googleapis.com/youtube/v3"
ISO = re.compile(r"^P(?:(\d+)D)?T?(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$")

def iso_to_seconds(iso: str) -> int:
    m = ISO.match(iso or "")
    if not m:
        return 0
    d, h, mi, s = (int(x or 0) for x in m.groups())
    return d * 86400 + h * 3600 + mi * 60 + s

def get(resource, **params):
    r = requests.get(f"{API}/{resource}", params={**params, "key": KEY}, timeout=20)
    r.raise_for_status()
    return r.json()

def playlist_duration(playlist_id: str):
    ids, token = [], None
    while True:
        page = get("playlistItems", part="contentDetails", playlistId=playlist_id,
                   maxResults=50, pageToken=token,
                   fields="nextPageToken,items/contentDetails/videoId")
        ids += [i["contentDetails"]["videoId"] for i in page["items"]]
        token = page.get("nextPageToken")
        if not token or len(ids) >= 5000:
            break
    total = unavailable = 0
    for i in range(0, len(ids), 50):
        batch = ids[i:i + 50]
        data = get("videos", part="contentDetails", id=",".join(batch),
                   fields="items(id,contentDetails/duration)")
        found = {v["id"]: iso_to_seconds(v["contentDetails"]["duration"]) for v in data["items"]}
        for vid in batch:
            s = found.get(vid, 0)
            if s > 0:
                total += s
            else:
                unavailable += 1
    return {"videos": len(ids), "unavailable": unavailable, "total_seconds": total}

Things that bite

Which option?

Use Option A for a script, a Discord bot, a spreadsheet formula (=IMPORTDATA won't parse JSON, but Apps Script will), or anything where you don't want to own an API key and a cache. Use Option B when you need data we don't expose, want sub-24-hour freshness, or are building a product of your own.