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"
}
?fields=summarydrops thevideosarray (much smaller).?from=10&to=25totals a range;?speed=1.6adds that speed toatSpeed.GET /api/v1/video/{VIDEO_ID}does the same for one video;GET /api/v1/channel/@handletotals a channel's uploads and lists its playlists.GET /api/v1/resolve?q=<pasted text>turns any YouTube URL, ID or@handleinto typed items without spending quota.- Results are cached for up to 24 hours;
fetchedAtsays when. Unavailable videos have"unavailable": trueandseconds: 0and are excluded from totals. - Errors come back as
{ "error": "not_found" | "quota_exceeded" | "rate_limited" | … , "message": "…" }with matching HTTP status codes;429and503carry aRetry-After. - CORS is open, so it works from a browser too. Limit: 60 requests a minute per IP.
- Just want a number in a README? The badge does it without any code.
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
- Durations are ISO 8601 (
PT1H2M3S,PT45S,P1DT2Hfor 26-hour streams). Don't parse them with a date library; the regex above handles what YouTube actually emits. - Unavailable videos appear in
playlistItemsbut are simply missing from thevideos.listresponse — hence the "found / not found" bookkeeping. Live and upcoming videos returnPT0S. - Pagination is sequential. Each page token comes from the previous page, so a 5,000-video playlist is 100 round trips. Start the
videos.listcall for page k while fetching page k+1 to halve the wall time. fieldsdoesn't reduce quota cost, but it shrinks responses a lot — worth it on serverless platforms with CPU limits.- Quota errors arrive as HTTP 403 with reason
quotaExceeded; the quota resets at midnight Pacific time. Cache aggressively (we use 24 hours) and you'll rarely see it. - Terms of service: don't store API data for more than 30 days, link to YouTube, and put a privacy policy on anything public. Details in Google's YouTube API Services terms.
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.