How do you get YouTube video search data via API?
Call ReefAPI's youtube search action with a query and read video ids, channels, view counts and thumbnails back as JSON, then enrich with video_detail or transcript. No OAuth and no quota units. The field to be careful with is views: in search results it is not always a view count.
This guide demonstrates the real YouTube API engine with a captured response from . The example is only published because the engine passed the SEO snapshot gate.
Video research, creator analytics, content monitoring and media discovery.
Call the live endpoint
- 1
Search with an explicit type
type defaults to 'all', which mixes videos, channels and playlists into one array. Set type 'video' when you want a homogeneous result set, and check the per-row type field anyway.
- 2
Read views and views_int together
If views ends in 'watching' the row is a live stream and views_int is a concurrent audience. Exclude those rows before aggregating, or you will mix snapshots into totals.
- 3
Enrich the rows you care about
Search gives relative dates and truncated descriptions. video_detail on the video_id returns the integer view count, likes, the full description and the channel's subscriber figure.
- 4
Check is_generated before using a transcript
is_generated true means auto-captions. On music, sport or silent footage those are mostly bracketed sound tags, and segment_count will happily report hundreds of them.
- 5
Page with continuation, not offsets
Send the continuation token from the previous response instead of query. meta.stop_reason tells you whether you hit your own limit or the end of the results.
Copy the request
These snippets use the captured request params for youtube/v1/search.
curl -X POST https://api.reefapi.com/youtube/v1/search \
-H "x-api-key: $REEF_KEY" \
-H "content-type: application/json" \
-d '{"query":"lofi hip hop","type":"video","limit":20}'import requests
r = requests.post(
"https://api.reefapi.com/youtube/v1/search",
headers={"x-api-key": REEF_KEY},
json={
"query": "lofi hip hop",
"type": "video",
"limit": 20
},
)
print(r.json()["data"])const res = await fetch("https://api.reefapi.com/youtube/v1/search", {
method: "POST",
headers: {
"x-api-key": process.env.REEF_KEY,
"content-type": "application/json",
},
body: JSON.stringify({
"query": "lofi hip hop",
"type": "video",
"limit": 20
}),
});
const { ok, data, meta, error } = await res.json();Ask your MCP-connected assistant: call reefapi.youtube.search with {"query":"lofi hip hop","type":"video","limit":20}.Captured output from ReefAPI
Captured on UTC. The response below is the committed snapshot, including the API envelope and metadata.
{
"method": "POST",
"url": "https://api.reefapi.com/youtube/v1/search",
"headers": {
"x-api-key": "$REEF_KEY",
"content-type": "application/json"
},
"body": {
"query": "lofi hip hop",
"type": "video",
"limit": 20
}
}{
"ok": true,
"meta": {
"api": "youtube",
"endpoint": "search",
"mode": "live",
"latency_ms": 1156.5,
"record_count": 20,
"bytes": 905226,
"cache_hit": false,
"completeness_pct": 100,
"stop_reason": "limit_reached",
"type": "video",
"estimated_results": 2425725,
"charged_credits": 1,
"version": "0.1.0"
},
"data": {
"results": [
{
"type": "video",
"video_id": "rFZHOHl-L8A",
"title": "lofi hip hop radio 📚 beats to relax/study to",
"channel": "Lofi Girl",
"channel_id": "UCSJ4gkVC6NrvII8umztf0Ow",
"views": "14,566 watching",
"views_int": 14566,
"published": null,
"length": null,
"description": "New Character Unlocked: discover Lofi Teacher in the new Lofi House radio! https://lnk.to/house_lofi02 | Listen on Spotify, ...",
"thumbnails": [
{
"url": "[trimmed-depth]",
"width": "[trimmed-depth]",
"height": "[trimmed-depth]"
},
{
"url": "[trimmed-depth]",
"width": "[trimmed-depth]",
"height": "[trimmed-depth]"
}
],
"url": "https://www.youtube.com/watch?v=rFZHOHl-L8A"
},
{
"type": "video",
"video_id": "n61ULEU7CO0",
"title": "Best of lofi hip hop 2021 ✨ [beats to relax/study to]",
"channel": "Lofi Girl",
"channel_id": "UCSJ4gkVC6NrvII8umztf0Ow",
"views": "57,551,758 views",
"views_int": 57551758,
"published": "4 years ago",
"length": "6:10:58",
"description": "Listen on Spotify, Apple music and more → https://fanlink.tv/BestofLofi2021 The new Lofi Girl compilation “Best of 2021” is out now ...",
"thumbnails": [
{
"url": "[trimmed-depth]",
"width": "[trimmed-depth]",
"height": "[trimmed-depth]"
},
{
"url": "[trimmed-depth]",
"width": "[trimmed-depth]",
"height": "[trimmed-depth]"
}
],
"url": "https://www.youtube.com/watch?v=n61ULEU7CO0"
},
{
"type": "video",
"video_id": "CLeZyIID9Bo",
"title": "Chill Lofi Mix [chill lo-fi hip hop beats]",
"channel": "Settle",
"channel_id": "UCkKT4qf-TcPFOmpqhTawrGA",
"views": "34,685,248 views",
"views_int": 34685248,
"published": "4 years ago",
"length": "1:44:52",
"description": "YouTube Playlists: http://listen.settle.fm/youtube Listen on Spotify, Apple Music & more: https://listen.settle.fm/playlists Tracklist ...",
"thumbnails": [
{
"url": "[trimmed-depth]",
"width": "[trimmed-depth]",
"height": "[trimmed-depth]"
},
{
"url": "[trimmed-depth]",
"width": "[trimmed-depth]",
"height": "[trimmed-depth]"
}
],
"url": "https://www.youtube.com/watch?v=CLeZyIID9Bo"
}
],
"count": 20,
"estimated_results": 2425725,
"continuation": "EvAEEgxsb2ZpIGhpcCBob3Aa3ARFcnNEa2dHM0F6b3JFaWQ1YjNWMGRXSmxYMnhwZG1WZlluSnZZV1JqWVhOMFgzTjBZWFIxY3owd0lEcDBlWEJsT25KZ0JFcUhBd29jQ2d4c2IyWnBJR2hwY0NCb2IzRHlBUVVLQTBGc2JOZ0NBYmdEWVFxbEFmSUJDQW9HVTJodmNuUnp3Z0tRQVNoaElIbHZkWFIxWW1WZmMyaHZjblJ6WDJSbFptbHVhWFJwYjI0Z09uUjVjR1U2Y2lBb2JpQjViM1YwZFdKbFgyWnNZV2RmYUdGelgzQnlaVzFwWlhKbFgzWnBaR1Z2WDIxbGRHRmtZWFJoUFRFZ09uUjVjR1U2Y2lrZ0tHNGdlVzkxZEhWaVpWOW1iR0ZuWDJoaGMxOXNhWFpsWDNOMGNtVmhiVjl0WlhSaFpHRjBZVDB4SURwMGVYQmxPbklwS2ZnQ0FiZ0RHZ29XOGdFTENnbFZibmRoZEdOb1pXVEtBZ0lZQWJnREhBb1U4Z0VKQ2dkWFlYUmphR1ZreWdJQ0dBSzRBMG9LT3ZJQkNBb0dWbWxrWlc5endnSWFlVzkxZEhWaVpW",
"query": "lofi hip hop",
"type": "video"
}
}Why this is hard manually
The mistake that costs the most is assuming a search result row means the same thing on every row. Searching 'lofi hip hop' sorted by date returned Lofi Girl's permanent radio stream first, and its views field read '14,610 watching' with views_int 14610. That is concurrent viewers, not lifetime views, sitting in the same field that held 135,152,722 on the row above it. Sum that column across a result set and you have added an audience snapshot to a lifetime total.
The same row also returned published null and length null, because a stream that has been running for years has neither an upload date nor a duration. So the upload_date filter cannot exclude it: we asked for 'week' plus sort by date and got a six-year-old stream at position one. The estimated result count did respond to the filter, dropping from 9,576,737 to 85,877, so the filter reached YouTube - it just has nothing to filter live streams on.
And there is no ISO timestamp anywhere. Search gives '6 years ago' or 'Streamed 5 days ago'; video detail gives 'Premiered Dec 8, 2019'. Both are human strings with prefixes, and both need parsing before they can be sorted.
Why ReefAPI solves it
Search returns both forms of every number so you never have to parse a label: views as the raw string ('135,152,722 views', or '14,610 watching' on a live stream) and views_int as the integer behind it. Reading views alongside views_int is the only reliable way to tell a view count from a concurrent-viewer count, because views_int alone cannot tell you which it is. If the string ends in 'watching', the number is an audience, not a total.
video_detail is where the absolute numbers live: views 135152722 as an integer plus views_text and views_short, likes 2100000, and a channel block with name, channel_id, handle (@LofiGirl), subscribers 15800000 and subscribers_text '15.8M subscribers'. Note that subscribers is derived from that display string, so it carries YouTube's rounding, not exact precision - 15,800,000 means 15.8M, not a precise count. meta.player_enriched tells you the player payload was reachable.
Dates are returned as YouTube writes them rather than reformatted into something that would be wrong. published_date came back 'Premiered Dec 8, 2019' - the 'Premiered' prefix is part of the fact, because it says the video was a scheduled premiere rather than a normal upload. published_relative carries '6 years ago' separately. Parse both; do not assume either is empty.
transcript reports its own quality before you read a word of it. On our test video it returned is_generated true, language_code 'en', segment_count 258 and duration_ms 3675599 - and the text was almost entirely '[Music]' and '[Applause]', because auto-captions on an hour of instrumental music have nothing to transcribe. is_generated false means human-authored captions. A transcript that comes back ok:true with 258 segments can still be useless, and segment_count will not tell you that; is_generated plus a glance at the text will.
Search paginates on a continuation token rather than page numbers. The response carries continuation, count, estimated_results and meta.stop_reason ('limit_reached' when your limit was the binding constraint). Feed continuation back in place of query to get the next page. limit accepts 1-200 and is paged internally, so a limit of 200 is one call to you and several to YouTube.
Eleven actions share the engine: search, video_detail, comments, transcript, channel, channel_about, channel_videos, channel_shorts, playlist, related and trending. Search returned in about 1.2 seconds, video detail in 3.6 and a 258-segment transcript in 2.8 in our runs. Thumbnails come as an array with explicit width and height rather than a single guessed URL.
Questions developers ask
Does this need OAuth or a Google Cloud project?
No. You send a ReefAPI key. There is no OAuth consent screen, no quota units and no per-project daily cap to budget around.
Why does views_int say 14,610 on a video with millions of views?
Because that row is a live stream and the number is people watching right now. The string form makes it visible: views reads '14,610 watching' instead of '14,610 views'. Live rows also come back with published null and length null.
The upload_date filter let a six-year-old video through. Why?
Live streams have no upload date for the filter to test. estimated_results did fall from 9,576,737 to 85,877 when we applied 'week', so the filter was applied - it simply cannot exclude a row that has no date. Filter live rows out yourself on length null.
Can I get an ISO 8601 publish date?
Not from YouTube's public surfaces. Search returns relative strings ('6 years ago', 'Streamed 5 days ago') and video_detail returns a display date that may carry a 'Premiered' prefix. We pass both through unchanged rather than guess at a timestamp.
My transcript came back full of [Music]. Is the transcript broken?
No - it is an auto-generated caption track on content with no speech. Check is_generated: true means YouTube's speech recognition produced it. Our test returned 258 segments across 61 minutes and almost every one was a sound tag.
Is the subscriber count exact?
No, and neither is YouTube's. subscribers 15800000 is derived from the displayed '15.8M subscribers'. Treat it as a rounded figure for ranking, not an exact number for reporting.
How many results can one search return?
limit accepts 1 to 200 and the engine pages internally to reach it. Past that, follow the continuation token. estimated_results is YouTube's own estimate of the total corpus and is not a promise of retrievable rows.