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
| Endpoint | Returns | Query 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 length | speed |
/resolve?q= | What any pasted text refers to (playlist, video, channel) — never calls YouTube | q — 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 "https://playlistduration.com/api/v1/playlist/PL8dPuuaLjXtNlUrzyH5r6jN9ulIgZBpdo?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.
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}`);
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']}")
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": "…" }:
| HTTP | error | Meaning |
|---|---|---|
| 400 | bad_request | The ID isn't well-formed |
| 404 | not_found | Doesn't exist, or is private (we don't distinguish) |
| 429 | rate_limited | Over 60 requests per minute from one IP — wait Retry-After seconds |
| 502 | upstream | YouTube's API returned an error; retry shortly |
| 503 | quota_exceeded | Our daily YouTube quota is spent; cached results still work. Retry-After counts to the reset (midnight Pacific) |
Rate limit and attribution
- 60 requests per minute per IP (window 60 s), counted per Cloudflare data centre. Cache on your side: playlists change slowly, and a cached response costs nobody anything.
- Link back. Every response includes a
url. Wherever you show these numbers to people, link to that page — it's what keeps the API free. The same request is in theX-Attributionheader. - Going past a few thousand requests a day? Tell us first so we can plan quota for it.
- By using the API you agree to our terms and, since the data comes from YouTube API Services, to the YouTube Terms of Service. Don't store the data for more than 30 days.
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:
[](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.