API

The numbers behind every page on this site, as JSON. Free, no key, CORS-enabled — built for scripts, bots, spreadsheets and course READMEs.

Base URL: https://playlistduration.com/api/v1 (the bare URL redirects here). Every API response carries Link: <https://playlistduration.com/developers>; rel="service-doc", so you can always find your way back from a raw response. All responses are JSON (UTF-8) and carry Cache-Control; results are cached for up to 24 hours on our side (fetchedAt says when). Only GET is supported.

Endpoints

EndpointReturnsQuery parameters
/playlist/{id}Totals, speed table and every video in the playlist (up to 5,000)fields=summary drops videos · from, to (1-based positions) total a range · speed adds that speed to atSpeed
/video/{id}One video's lengthspeed
/resolve?q=What any pasted text refers to (playlist, video, channel) — never calls YouTubeq — URLs, IDs or @handles, several at once
/channel/{@handle|id}Uploads total (newest first), speed table, uploads by year, longest uploads, and every public playlist on the channel (ids, titles, counts — fetch each length with /playlist/{id}?fields=summary)speed

Example

curl
curl "https://playlistduration.com/api/v1/playlist/PL8dPuuaLjXtNlUrzyH5r6jN9ulIgZBpdo?fields=summary"
Response (without fields=summary)
{
  "id": "PL8dPuuaLjXtNlUrzyH5r6jN9ulIgZBpdo",
  "url": "https://playlistduration.com/playlist/PL8dPuuaLjXtNlUrzyH5r6jN9ulIgZBpdo",
  "title": "Computer Science",
  "channel": { "id": "UCX6b17PVsYBQ0ip5gyeme-Q", "title": "CrashCourse" },
  "privacy": "public",
  "indexable": true,
  "itemCount": 41,
  "counted": 41,
  "unavailable": 0,
  "truncated": false,
  "totalSeconds": 28540,
  "averageSeconds": 696,
  "longest":  { "id": "…", "n": 12, "seconds": 837 },
  "shortest": { "id": "…", "n": 1,  "seconds": 358 },
  "atSpeed": { "1": 28540, "1.25": 22832, "1.5": 19027, "1.75": 16309, "2": 14270 },
  "videos": [
    { "n": 1, "id": "…", "title": "Early Computing: Crash Course Computer Science #1", "seconds": 358, "publishedAt": "2017-02-22T…" },
    { "n": 2, "id": "…", "title": "(unavailable)", "seconds": 0, "unavailable": true }
  ],
  "fetchedAt": "2026-09-22T10:00:00.000Z",
  "cache": "hit"
}

Durations are integer seconds. counted is the number of videos with a known length; unavailable ones (private, deleted, region-blocked) are listed with seconds: 0 and left out of every total. truncated is true when a playlist has more than 5,000 items. indexable is false for unlisted playlists — please don't publish those.

JavaScript
const res = await fetch("https://playlistduration.com/api/v1/playlist/PL8dPuuaLjXtNlUrzyH5r6jN9ulIgZBpdo?fields=summary");
if (!res.ok) throw new Error((await res.json()).message);
const { title, totalSeconds, counted, atSpeed, url } = await res.json();
console.log(`${title}: ${(totalSeconds / 3600).toFixed(1)} h across ${counted} videos (${(atSpeed["1.5"] / 3600).toFixed(1)} h at 1.5×) — ${url}`);
Python
import requests

r = requests.get("https://playlistduration.com/api/v1/playlist/PL8dPuuaLjXtNlUrzyH5r6jN9ulIgZBpdo", params={"fields": "summary"}, timeout=30)
r.raise_for_status()
data = r.json()
print(f"{data['title']}: {data['totalSeconds'] / 3600:.1f} h across {data['counted']} videos — {data['url']}")
Resolve
GET https://playlistduration.com/api/v1/resolve?q=https://www.youtube.com/watch?v=dQw4w9WgXcQ&list=PL8dPuuaLjXtNlUrzyH5r6jN9ulIgZBpdo

{ "items": [ { "kind": "playlist", "id": "PL8dPuuaLjXtNlUrzyH5r6jN9ulIgZBpdo", "videoId": "dQw4w9WgXcQ" } ], "unrecognized": [] }

Errors

Non-2xx responses are { "error": "…", "message": "…" }:

HTTPerrorMeaning
400bad_requestThe ID isn't well-formed
404not_foundDoesn't exist, or is private (we don't distinguish)
429rate_limitedOver 60 requests per minute from one IP — wait Retry-After seconds
502upstreamYouTube's API returned an error; retry shortly
503quota_exceededOur daily YouTube quota is spent; cached results still work. Retry-After counts to the reset (midnight Pacific)

Rate limit and attribution

Calendar feed

Every playlist page can also be subscribed to as a calendar: /calendar/playlist/{id}.ics?m=60&d=0,1,2,3,4&start=2026-09-28&t=19:00&speed=1.5&w=1-12 returns one event per study day (m minutes per day, d weekdays with 0 = Monday, w watched positions as ranges). The plan lives in the URL and is rebuilt from the live playlist on every poll, so nothing is stored and new videos become new sessions.

Badge

An SVG that shows a playlist's watch time and updates itself — for a README, a course description or a wiki. Wrap it in a link to the result page:

Watch time badge example

Markdown
[![Watch time](https://playlistduration.com/badge/playlist/PL8dPuuaLjXtNlUrzyH5r6jN9ulIgZBpdo.svg)](https://playlistduration.com/playlist/PL8dPuuaLjXtNlUrzyH5r6jN9ulIgZBpdo)

Options on the image URL: ?speed=1.5 shows the time at that speed, ?label=Course changes the left text, ?style=flat-square squares the corners. /badge/video/{id}.svg and /badge/channel/{@handle}.svg work the same for a video or a channel. Badges are cached for an hour and answer errors with a grey "not found" badge rather than a broken image.

Versioning

/api/v1/ is stable: fields may be added, never removed or renamed. Anything breaking becomes /api/v2/. The unversioned /api/ paths are for our own pages and can change without notice.

Rather call YouTube yourself?

The guide YouTube playlist duration API walks through the three Data API calls, the ISO-8601 durations and the quota maths, with JavaScript and Python you can paste.