2025, Dec 14 19:00

Fetch All YouTube Channel Videos Reliably: Use the Uploads Playlist via playlistItems.list

Learn why search.list fails for channel enumeration and how to fetch YouTube uploads via the uploads playlist using playlistItems.list for stable paging.

When you need a reliable list of videos from a YouTube channel, calling search.list with channelId and a larger maxResults may look straightforward. In practice, it can return only a handful of items, fluctuate between calls, and provide no pagination token. If your goal is to fetch all uploads or at least the newest 30–40, there’s a more stable and cheaper path in terms of API quota.

Problem setup

The initial approach uses the YouTube Data API search endpoint with parameters like API key, channelId, part=id, and maxResults=30. Despite that, the response may contain only five items, with inconsistent counts across repeated requests, and without pagination.

import requests
def demo_channel_search(api_key, channel_id):
    url = (
        f"https://www.googleapis.com/youtube/v3/search?"
        f"part=id&channelId={channel_id}&maxResults=30&key={api_key}"
    )
    print(f"Calling search endpoint: {url}")
    resp = requests.get(url)
    data = resp.json()
    items = data.get("items", [])
    print(f"Items returned: {len(items)}")
    print(f"nextPageToken: {data.get('nextPageToken')}")

What’s really going on

The search endpoint isn’t designed to enumerate a channel’s full upload history. It can return fewer items than requested and behave inconsistently for this use case. YouTube channels have a dedicated uploads playlist that contains all videos published by that channel. Accessing that playlist directly is the canonical way to retrieve the complete list of videos and paginate through them. There is also a clear quota advantage: playlistItems.list costs 1 quota unit per call, whereas search.list costs 100 units per call.

The fix: query the uploads playlist

The solution has two steps. First, query the channel’s contentDetails to obtain the uploads playlist ID. Second, iterate that playlist via playlistItems.list to collect videos, following nextPageToken until exhaustion.

import requests
def resolve_uploads_feed_id(chan_id, key_token):
    endpoint = (
        f"https://www.googleapis.com/youtube/v3/channels?"
        f"part=contentDetails&id={chan_id}&key={key_token}"
    )
    print(f"Requesting uploads playlist via: {endpoint}")
    try:
        resp = requests.get(endpoint)
        payload = resp.json()
        if 'error' in payload:
            print(f"Error: {payload['error']['message']}")
            return None
        if 'items' in payload and len(payload['items']) > 0:
            upl_id = payload['items'][0]['contentDetails']['relatedPlaylists']['uploads']
            print(f"Uploads playlist resolved: {upl_id}")
            return upl_id
        else:
            print(f"No channel found for: {chan_id}")
            return None
    except Exception as exc:
        print(f"Unexpected failure: {exc}")
        return None
def collect_videos_from_uploads(list_id, key_token):
    if not list_id:
        print("Uploads playlist ID is missing")
        return []
    collected = []
    token = None
    print(f"Fetching videos from uploads playlist: {list_id}")
    while True:
        base = (
            f"https://www.googleapis.com/youtube/v3/playlistItems?"
            f"part=snippet&maxResults=50&playlistId={list_id}&key={key_token}"
        )
        url = f"{base}&pageToken={token}" if token else base
        print(f"Issuing playlistItems request: {url}")
        try:
            resp = requests.get(url)
            data = resp.json()
            if 'error' in data:
                print(f"Error: {data['error']['message']}")
                break
            items = data.get('items', [])
            print(f"Page size: {len(items)}")
            for it in items:
                entry = {
                    'video_id': it['snippet']['resourceId']['videoId'],
                    'title': it['snippet']['title'],
                    'published_at': it['snippet']['publishedAt'],
                }
                collected.append(entry)
            token = data.get('nextPageToken')
            if not token:
                print("Reached end of playlist")
                break
            else:
                print(f"Advancing with page token: {token}")
        except Exception as exc:
            print(f"Unexpected failure: {exc}")
            break
    return collected
if __name__ == "__main__":
    API_TOKEN = "###"
    CHANNEL_ID = "UCupvZG-5ko_eiXAupbDfxWw"
    uploads_id = resolve_uploads_feed_id(CHANNEL_ID, API_TOKEN)
    print(f"Uploads playlist ID: {uploads_id}")
    videos = collect_videos_from_uploads(uploads_id, API_TOKEN)
    print(f"Total videos fetched: {len(videos)}")
    for idx, v in enumerate(videos[:5]):
        print(f"Video {idx+1}:")
        print(f"  ID: {v['video_id']}")
        print(f"  Title: {v['title']}")
        print(f"  Published: {v['published_at']}")

Why this matters

Targeting the uploads playlist solves the inconsistency of the search endpoint for channel enumeration and gives you predictable pagination through nextPageToken. It also reduces cost: playlistItems.list uses 1 quota unit per call compared to 100 for search.list, which is significant when scanning channels or pulling large histories.

Takeaways

If you need a channel’s video list, don’t rely on search.list for enumeration. Resolve the uploads playlist via channels?part=contentDetails, then iterate it with playlistItems.list. You’ll get the complete set of uploads, stable paging, and a far more efficient quota profile. Keep maxResults at 50 for playlistItems and loop until there’s no nextPageToken if you want the full history, or stop early if you only need the latest N entries.