# YouTube (/docs/youtube) YouTube channels, videos, comments, shorts, playlists, community posts, transcripts, media files, and search through one unified API The widest surface on the API after LinkedIn: every tab of a channel, full video detail, threaded comments, two kinds of search, trending, transcripts and subtitle files, the raw audio/video/thumbnail streams, and batch endpoints that take a thousand ids at a time. Base URL: `/v1/youtube/...` Choosing between `/v1/youtube/video/subtitles` (1cr caption files) and `/v1/youtube/video/transcript` (3cr cleaned transcript)? See [Which endpoint should I use?](/docs/choosing-endpoints.md). ## How do I get started with YouTube data? ### 1. Fetch a profile ```bash title="cURL" curl "https://www.socialcrawl.dev/v1/youtube/channel?handle=MrBeast" \ -H "x-api-key: YOUR_API_KEY" ``` ### 2. Fetch their content ```bash title="cURL" curl "https://www.socialcrawl.dev/v1/youtube/channel/videos?handle=MrBeast" \ -H "x-api-key: YOUR_API_KEY" ``` ### 3. Read computed fields When an endpoint supports a computed field and the required source inputs are present, the unified response includes that optional field. Depending on the endpoint, optional fields can include `engagement_rate`, `language`, `content_category`, and `estimated_reach`. See [Computed fields](/docs/computed-fields.md) for formulas, clamping rules, and null semantics. Where an endpoint supports them, list rows also carry judged labels (`computed.labels`, `computed.relevance`) by default at no extra credits; see [Labels](/docs/labels.md). ## A channel has five tabs, and each has its own endpoint `GET /v1/youtube/channel/videos` returns **regular uploads only**. Everything else on a channel lives behind its own call, and this is the single most common cause of "where did the rest of their content go": | What you want | Endpoint | | --------------------- | ----------------------------------------- | | The account snapshot | `GET /v1/youtube/channel` | | Regular uploads | `GET /v1/youtube/channel/videos` | | Shorts | `GET /v1/youtube/channel/shorts` | | Live and past streams | `GET /v1/youtube/channel/lives` | | Playlists | `GET /v1/youtube/channel/playlists` | | Text and image posts | `GET /v1/youtube/channel/community-posts` | All five channel-scoped endpoints take **either `channelId` or `handle`** (one is required), and all cost 1 credit on a plain call. `channel` additionally accepts a `url`. On the videos, Shorts and live lists, `include=channel` adds the channel's subscriber count to every row for 1 more credit, and on the live list `include=engagement` adds the like and comment counts for at most 5 more. They page with `continuationToken`, YouTube's own token rather than the universal cursor: pass back what the previous response returned. `channel/videos` returns light rows by default. Add `includeExtras=true` to fill in `post.engagement.likes`, `post.engagement.comments`, and `post.ext.description` on each row. It slows the response slightly but saves a `video` call per row. ```bash title="cURL" # Shorts, not uploads curl "https://www.socialcrawl.dev/v1/youtube/channel/shorts?handle=MrBeast" \ -H "x-api-key: YOUR_API_KEY" # Uploads with engagement counts attached curl "https://www.socialcrawl.dev/v1/youtube/channel/videos?handle=MrBeast&includeExtras=true&sort=popular" \ -H "x-api-key: YOUR_API_KEY" ``` `GET /v1/youtube/profile/full` (5cr) collapses the first two rows into one call: the channel, its recent videos, and computed metrics. Two average engagement rates (`avg_engagement_rate` over view counts, `avg_engagement_rate_by_followers` over the subscriber count), posting cadence with the window it was measured over, the top post, and the format mix. `posts` sets how many videos to average over (1-100, default 25). The profile leg is the only critical one, so a failure to fetch videos still returns the profile with null post-dependent metrics and a `legs[]` explaining why. ## Two searches, and they return different things - **`GET /v1/youtube/search`** (1cr) is the general one. It returns **every content kind** (videos, channels, playlists, shorts, live streams) with `uploadDate`, `sortBy`, `type`, `duration`, and `region` filters. - **`GET /v1/youtube/search/advanced`** (1cr) returns **videos only**, but with the full filter set: `published_after` / `published_before` windows, `license`, `category`, `event_type`, `video_caption`, `video_definition`, `video_dimension`, `video_embeddable`, `topic_id`, `location` with `location_radius`, `safe_search`, and more. Reach for it when a plain keyword search is too blunt, and for `search` when you need channels or playlists back too. Both search calls are metered: the base page is 1 credit and the optional extras are added on top. On `search/advanced`, **`includeExtras=true` adds a flat 5 credits** to fill the page with view, like, and comment counts, because that is a batch call under the hood; the page caps at 50 results upstream, so the increment is a single flat +5, never a multiple. On both calls, `include` adds at most 5 credits per value (see [Subscriber counts next to search results](#subscriber-counts-next-to-search-results) below). So `search/advanced` is 1, 6 or 11 credits, and `search` is 1, 6 or 11. Two more search surfaces: `GET /v1/youtube/search/hashtag` (1cr) reads a hashtag feed rather than a keyword query, with `type=shorts` to restrict it, and `GET /v1/youtube/search/suggestions` (1cr) returns YouTube's own autocomplete list for a partial query, the cheapest keyword-research call on the platform. **`search`'s `sortBy=popular` is not a view-count sort.** It is a popularity-weighted ranking, and results do not come back in descending view order, a `new york` search returned 9.5M, then 502.9M, then 66K views in that order, on 4 of 4 test keywords. It also caps the page at roughly 20 results and cannot be combined with `type=shorts` (that pair is rejected with a `400`). For a view-faithful ordering use `search/advanced` with `order=viewCount&includeExtras=true`, then sort the page yourself on the returned `engagement.views`. Separately, `search`'s `duration` filter is currently not applied by the provider and can degrade the `type` filter. Filter client-side on `post.content.duration_seconds` instead. ```bash title="cURL" # Videos from a date window, with counts hydrated curl "https://www.socialcrawl.dev/v1/youtube/search/advanced?query=ai%20agents&published_after=2026-06-01&order=viewCount&includeExtras=true" \ -H "x-api-key: YOUR_API_KEY" # What does YouTube think people are searching for? curl "https://www.socialcrawl.dev/v1/youtube/search/suggestions?query=how%20to%20train" \ -H "x-api-key: YOUR_API_KEY" ``` ## One video, and everything hanging off it - **`GET /v1/youtube/video`** (1cr), the complete record from a video `url`: title, view/like/comment counts, description, tags, duration, channel, publish date. List endpoints return lighter rows without the description or tags, so this is the call when those matter. - **`GET /v1/youtube/video/comments`** (1cr): top-level comments with author, text, like count, reply count, and timestamp. `order`, `searchTerm`, and `max_results` narrow it. - **`GET /v1/youtube/video/comment/replies`** (1cr), one thread opened up. It takes the **`continuationToken` that `video/comments` returned for that comment**, not a comment id, and you keep paging until no replies remain. - **`GET /v1/youtube/video/transcript`** (3cr), the spoken words as timestamped segments with language, word count, and speech rate. A video without captions returns a `404` naming the reason; that is the expected result, not a failure. - **`GET /v1/youtube/video/subtitles`** (1cr), the caption _track files_ instead: language code and name, format, and a direct download URL, auto-generated tracks included. Cheaper than `transcript` when you want files rather than text. - **`GET /v1/youtube/video/sponsors`** (10cr): whether a video carries a paid-promotion disclosure, plus the brands likely sponsoring it, each with supporting evidence and a confidence score. Sponsors are **inferred** from the description, links, promo codes, and transcript, not stated by YouTube, which is why every row carries confidence. - **`GET /v1/youtube/video/audio`** (5cr), **`GET /v1/youtube/video/files`** (5cr), **`GET /v1/youtube/video/thumbnails`** (1cr), the downloadable streams. Audio gives mime type, bitrate, quality, sample rate and channels; files gives resolution, quality label, frame rate and bitrate; thumbnails gives every available size with width, height, and format. The media URLs from `video/audio` and `video/files` are **time-limited**. Fetch them immediately; do not store them in a database and expect them to resolve later. Store the video id and re-request when you need the bytes. ```bash title="cURL" # The whole comment tree on one video curl "https://www.socialcrawl.dev/v1/youtube/video/comments?url=https://www.youtube.com/watch?v=dQw4w9WgXcQ&order=top" \ -H "x-api-key: YOUR_API_KEY" # Then open one thread with the token that came back curl "https://www.socialcrawl.dev/v1/youtube/video/comment/replies?continuationToken=TOKEN_FROM_PREVIOUS_RESPONSE" \ -H "x-api-key: YOUR_API_KEY" ``` ## Comments from a date window There is no date parameter on `video/comments`, because YouTube exposes none for comments, a server-side filter would read the same pages at the same cost. What makes a window pull work instead is that `order=newest` is an exact sort: rows arrive newest-first on `comment.published_at` with per-second timestamps, and each next page continues strictly older with no overlap. So to pull, say, the last two years of comments on a video: 1. Request `order=newest`, up to 100 comments per credit. 2. Keep rows where `comment.published_at` is on or after your cutoff. 3. Stop paging when the **last** row of a page is older than the cutoff. The one row that can defy the sort is a pinned comment, which YouTube serves first regardless of `order`, so filter rows by timestamp (step 2) rather than stopping at the first old row you see, and step 3 stays correct. ```bash title="cURL" curl "https://www.socialcrawl.dev/v1/youtube/video/comments?url=https://www.youtube.com/watch?v=dQw4w9WgXcQ&order=newest" \ -H "x-api-key: YOUR_API_KEY" ``` The same shape works on the replies side: `video/comment/replies` rows carry the same `comment.published_at`, so apply the cutoff to the rows as you drain each thread. ## Playlists and community posts `GET /v1/youtube/playlist` and `GET /v1/youtube/playlist/items` (1cr each) both take a `playlist_id` and return the same rows from the same source, in playlist order: video id, title, thumbnail, the video's own channel (`post.author.display_name`, `post.ext.channel_id`) and its publish date, with the position and the insertion time on `post.ext`. On a plain call neither carries view, like or comment counts or durations, because YouTube's playlist listing does not include them. Add `include=engagement` and every row gets them in the same call from the per-video lookup, and `include=channel` adds each video's channel subscriber count: at most 5 credits per join for a 50-row page, refunded for anything that could not be filled. The one difference between the two endpoints is availability: `playlist` has a second source behind it, so prefer it when uptime matters. `GET /v1/youtube/community-post` (1cr) takes a single post `url`; `GET /v1/youtube/channel/community-posts` lists them for a whole channel. ## Trending `GET /v1/youtube/videos/trending` (1cr) takes `region`, `category`, `language`, and `max_results`. This is the filterable one. `GET /v1/youtube/shorts/trending` (5cr) is a Shorts snapshot and takes only `include`, so it is one global list rather than a per-country view. Both accept `include=channel`, which adds each row's channel subscriber count for at most 5 more credits on trending videos and 10 on trending Shorts, whose page runs to about 70 rows. ## Batch: one call, up to a thousand ids Three `POST` endpoints take a JSON body when you already hold ids and want to stop calling the singular endpoint in a loop: - **`POST /v1/youtube/videos`**: up to 1000 video ids, each row the same object `/v1/youtube/video` returns. Ids that do not resolve are simply absent from the list. - **`POST /v1/youtube/channels`**: up to 1000 channel ids, each row the same channel object `/v1/youtube/channel` returns, **subscriber count included** (`author.followers`). Ids that do not resolve are absent from the list rather than returned empty, so **join on `author.id`, never on array position**, the response can be shorter than the request. Note that only the singular `channel` endpoint accepts a handle or URL; the batch takes bare `UC…` ids and rejects anything else with a `400` before billing. - **`POST /v1/youtube/transcripts`**: up to 100 video ids, one transcript row each with the caption language it resolved to and an `ok` or not-found status. A video without captions comes back not-found rather than failing the request, and **failed ids are refunded**. Flat 3cr. `videos` and `channels` are metered **per chunk of 50 ids at 5 credits a chunk**: 50 ids is 5cr, 200 ids is 20cr, the full 1000 is 100cr. The charge is per chunk regardless of how many ids in it resolve, so there is no per-id refund on these two. ```bash title="cURL" curl -X POST "https://www.socialcrawl.dev/v1/youtube/transcripts" \ -H "x-api-key: YOUR_API_KEY" \ -H "content-type: application/json" \ -d '{"ids":["dQw4w9WgXcQ","9bZkp7q19f0"],"language":"en"}' ``` ## Subscriber counts next to search results Add `include=channel` and every result carries its channel's subscriber count on `post.ext.author_followers`, in the same call. Add `engagement` too and Shorts results, which arrive without a channel, get one first. ```bash title="cURL" curl "https://www.socialcrawl.dev/v1/youtube/search?query=iphone17&type=shorts&include=engagement,channel" \ -H "x-api-key: YOUR_API_KEY" ``` `include=engagement` fills the like and comment counts, the duration and `post.ext.channel_id` from the per-video lookup. On `search`, `search/hashtag`, `channel/videos` and `channel/lives`, the exact publish time is already on the plain call, at no extra credit. `exact_dates=false` keeps the supplier's estimate. `include=engagement` still replaces a remaining estimate on the other lists, including Shorts and playlists. `include=channel` then adds the subscriber count and fills the channel's name and avatar where a row lacks them. Each join costs 1 credit per distinct video or channel it filled, and never more than 5 per 50. A page of Shorts with both costs at most 11 credits, the same as searching, resolving the videos and resolving the channels in three separate calls. Anything that could not be filled is refunded, and the `hydration` block on the response lists the rows, lookups, credits kept and milliseconds. The same two values work on `search/hashtag`, `playlist`, `playlist/items` and `channel/lives`. `include=channel` alone works on `search/advanced`, `videos/trending`, `shorts/trending`, `channel/videos` and `channel/shorts`. On the three channel lists every row is the same channel, so the join is one lookup and costs 1 credit. Which surface carries the channel id on a plain call: | Search call | Carries `post.ext.channel_id`? | | -------------------- | ------------------------------------------------------- | | `search?type=videos` | Yes, on every result | | `search/advanced` | Yes, on every result, no `includeExtras` needed | | `search?type=shorts` | No, until `include=engagement` joins it onto every row | **Shorts results carry no creator on a plain call.** YouTube's Shorts shelf ships the video without a channel object, so `post.author.username`, `post.author.display_name`, `post.author.avatar_url`, and `post.ext.channel_id` are null on every `type=shorts` result, and `includeExtras=true` does not change it. `include=engagement` fills the channel id and name on every one from the per-video lookup. For a crawl that meets the same channels on many pages, it is cheaper to collect the channel ids across the whole run, deduplicate them, and resolve them once: `POST /v1/youtube/channels` takes up to 1,000 ids at 5 credits per 50. ```bash title="cURL" curl -X POST "https://www.socialcrawl.dev/v1/youtube/channels" \ -H "x-api-key: YOUR_API_KEY" \ -H "content-type: application/json" \ -d '{"ids":["UCn9l4gU5mkmmlC2eiVu0LHw","UC_x5XG1OV2P6uZZ5FSM9Ttw"]}' ``` `author.followers` is YouTube's own published number, and **YouTube rounds it to three significant figures** above 1,000 subscribers. You will see 12,800,000 and 85,700 and 1,240, never the digit-exact figure behind them. That rounding is YouTube's, applied to everyone, so there is no exact count being withheld from you; below 1,000 subscribers it is exact. Channels at or above 1,000 subscribers also set `author.ext.followers_approximate` to `true`. A channel that hides its count entirely sets `author.ext.hiddenSubscriberCount` to `true`, which is the signal to drop it from a ratio rather than treat a zero as real. ## Labels These lists label their rows by default, at no extra credit. On `/v1/youtube/video/comments` and `/v1/youtube/video/comment/replies`, each comment carries `sentiment`, `question`, `purchase_intent` and `complaint` on `computed.labels`. On `/v1/youtube/search`, each row carries `sponsored`, `intent` and `niche`. `/v1/youtube/search` also scores each row's relevance to your `query` on `computed.relevance`, without dropping or reordering anything; add `relevance=filter` to drop the rows that are about something else, still free. Send `judgments=off` for the page without them. See [Labels](/docs/labels.md) for the field shapes, rows still `pending`, and the labels you can add with `label=`. ## What YouTube data can I access? ## What should I know about YouTube data? - Everything is `GET` with query parameters except the three batch endpoints (`videos`, `channels`, `transcripts`), which take a `POST` JSON body - Authentication via `x-api-key` header - Responses follow the unified SocialCrawl schema - Channel-scoped endpoints take `channelId` **or** `handle`; video-scoped endpoints take the video `url`; playlist endpoints take `playlist_id` - Pagination is `continuationToken` on the channel, search, comments, and hashtag endpoints, and `cursor` on `playlist`, `playlist/items`, `videos/trending`, and `search/advanced`. Pass back whatever the previous response returned rather than assuming one name - `hl` on `channel`, `video`, and the two id-batch endpoints sets the localisation language of the returned metadata ## Official YouTube Resources - [YouTube Data API v3](https://developers.google.com/youtube/v3): Official Google developer documentation - [YouTube Analytics API](https://developers.google.com/youtube/analytics): Channel and video analytics