YouTubeAnswer

YouTube API: get a channel's community posts

The YouTube Data API has no resource for community posts. Our API returns a channel's Posts tab, 10 posts a call, with likes, comments and poll votes.

5 min read

Short answer

  • The YouTube Data API has no resource for community posts, and activities.list stopped returning channel bulletins when YouTube retired them.
  • A channel's Posts tab is public, so its text, images, polls, quizzes and shared videos can be read without the owner's sign-in.
  • Our YouTube API returns the Posts tab, 10 posts a call at 1 credit, with like and comment counts and a poll's total votes.
On this page

The YouTube Data API can't read community posts. Its reference has no resource for the Posts tab, and activities.list stopped returning channel bulletins once YouTube retired them. Our YouTube API reads a channel's public Posts tab instead, 10 posts a call, with text, attachments, likes, comments and poll votes.

What the YouTube Data API is for

The YouTube Data API has resources for videos, channels, playlists, comments, captions, subscriptions and activities, and none for posts. activities.list used to include channel bulletins, the older form of a post. YouTube deprecated bulletins, and the method no longer returns them. Posts now come in text, image, video, poll, quiz and playlist types, and none of them is in the API.

Community posts with ToolzerHub

We make ToolzerHub. Our YouTube API returns public YouTube channels, videos and comments as JSON with one API key and no Google project. Two routes cover this, each 1 credit a call:

  • /v2/youtube/resolve: turns a channel link or @handle into its channel ID
  • /v2/youtube/channel/community: the Posts tab, 10 posts a page, with page_cursor for the next 10
bash
export TOOLZERHUB_API_KEY=your_key
curl -H "x-api-key: $TOOLZERHUB_API_KEY" \
  "https://api.toolzerhub.com/v2/youtube/channel/community?channel_id=UCX6OQ3DkcsbYNE6H8uQQuVA"

A poll post from MrBeast's tab in data.data:

json
{
  "type": "post",
  "postId": "UgkxZusu9I1Z-VuU5PGZNA2gclHi8V9CJVZk",
  "contentText": "Would you rather stop aging for 100 years or receive 1 billion dollars right now?",
  "publishedTimeText": "3 months ago",
  "publishDate": "2026-08-04",
  "voteCountText": "108K",
  "replyCount": "10K",
  "attachment": {
    "type": "poll",
    "totalVotes": "1.8M votes",
    "choices": ["100 Years Of No Aging", "1 Billion Dollars"]
  }
}

voteCountText is likes and replyCount is comments, both rounded the way YouTube shows them. publishDate comes from the relative time, so a post marked 2 years ago gets the right year, while its month and day are the day you call. A poll gives its total votes and choices, not the split between them.

Export a channel's posts to CSV

The script takes channel handles, links or IDs, reads the pages you ask for and writes one row per post with its kind, likes, comments and poll votes. For each channel it prints how far back the posts go, the median likes and the mix of post kinds. Install requests (pip install requests) and save it as youtube_community_posts.py.

python
import collections
import csv
import os
import re
import statistics
import sys
import time

import requests

API = "https://api.toolzerhub.com/v2/youtube"
HEADERS = {"x-api-key": os.environ.get("TOOLZERHUB_API_KEY", "")}
FIELDS = ["channel", "post_id", "published", "kind", "likes", "comments", "poll_votes", "text", "url"]


def get(op, params, tries=3):
    # Retry rate limits, server errors and dropped connections.
    for attempt in range(tries):
        last = attempt == tries - 1
        try:
            r = requests.get(f"{API}/{op}", headers=HEADERS, params=params, timeout=120)
        except requests.RequestException:
            if last:
                raise
            time.sleep(5 * (attempt + 1))
            continue
        if r.status_code in (429, 500, 502, 503, 504) and not last:
            time.sleep(5 * (attempt + 1))
            continue
        r.raise_for_status()
        return r.json()["data"]


def count(text):
    # YouTube shows counts as text such as "11K", "3.4M" or "1.8M votes"; "No votes" gives None.
    m = re.fullmatch(r"([\d.]+)([KMB]?)", str(text or "").split(" ")[0].replace(",", "").upper())
    if not m:
        return None
    return round(float(m[1]) * {"K": 1_000, "M": 1_000_000, "B": 1_000_000_000}.get(m[2], 1))


def channel_id(ref):
    if ref.startswith("UC") and len(ref) == 24:
        return ref
    url = ref if "youtube.com/" in ref else f"https://www.youtube.com/@{ref.lstrip('@')}"
    return get("resolve", {"youtube_url": url})["browseId"]


def main(channels, pages="3", out="youtube_community_posts.csv"):
    with open(out, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=FIELDS)
        writer.writeheader()
        for ref in filter(None, (c.strip() for c in channels.split(","))):
            cid, posts, cursor, name = channel_id(ref), [], None, ref
            for _ in range(int(pages)):
                data = get("channel/community", {"channel_id": cid, **({"page_cursor": cursor} if cursor else {})})
                name = (data.get("meta") or {}).get("title") or name
                posts += data.get("data") or []
                cursor = data.get("page_cursor")
                if not cursor:
                    break
            posts = [p for p in posts if p.get("postId")]
            for p in posts:
                a = p.get("attachment") or {}
                writer.writerow({
                    "channel": name, "post_id": p["postId"], "published": p.get("publishDate"),
                    "kind": a.get("type", "text"), "likes": count(p.get("voteCountText")),
                    "comments": count(p.get("replyCount")), "poll_votes": count(a.get("totalVotes")),
                    "text": p.get("contentText") or "", "url": f"https://www.youtube.com/post/{p['postId']}",
                })
            kinds = collections.Counter((p.get("attachment") or {}).get("type", "text") for p in posts)
            likes = [n for n in (count(p.get("voteCountText")) for p in posts) if n is not None]
            dates = [p["publishDate"] for p in posts if p.get("publishDate")]
            print(f"{name}: {len(posts)} posts back to {min(dates) if dates else '-'}, "
                  f"median {statistics.median(likes or [0]):,.0f} likes; "
                  + ", ".join(f"{k} {n}" for k, n in kinds.most_common()))


if __name__ == "__main__":
    if len(sys.argv) < 2:
        sys.exit('usage: python youtube_community_posts.py "@MrBeast,@NASA" [pages] [out.csv]')
    if not HEADERS["x-api-key"]:
        sys.exit("set TOOLZERHUB_API_KEY first")
    main(*sys.argv[1:])

What five channels showed

python youtube_community_posts.py "@MrBeast,@mkbhd,@veritasium,@NASA,@Fireship" 3 made 20 calls in 11 seconds and saved 140 posts. MKBHD's third page came back empty, so its tab gave 20.

Image posts count image and multi_image together.

Channel Posts Image posts Their median likes Video posts Their median likes Polls and quizzes
MrBeast 30 23 421,000 2 40,000 3
MKBHD 20 15 39,000 4 3,900 0
Veritasium 30 18 5,200 3 2,100 8
NASA 30 2 68,500 25 1,000 0
Fireship 30 11 7,800 5 1,700 12

Image posts got more likes than video posts on all five channels, about 10 times more on MrBeast and MKBHD, though the video samples are small. 25 of NASA's 30 posts are videos, and none of its 30 shows a comment count. Fireship runs a poll in 12 of 30 posts, at 36,000 to 289,000 votes each. Veritasium's three quizzes come back with no date.

What it costs

A channel is 4 credits at three pages: one to resolve the handle and one per page of 10. The 100 free credits a month cover 25 channels at that depth; $19 buys 10,000 credits that never expire, and plans for higher volume cover more channels. Set your key and the first channel's posts are a CSV in seconds.

For a channel's Shorts, see get a channel's Shorts. For comments under videos, see get all comments on a YouTube video.

We are not affiliated with YouTube or Google.

Get this data from your own code

The YouTube API returns it as JSON with one x-api-key header. 100 free credits monthly.