Search YouTube Programmatically with a Single API
David Boulen · 7/9/2026 · 7 min read
Search YouTube Programmatically with a Single API
Every content ingestion pipeline starts the same way: you need a list of videos to process. You might be building a summarizer, feeding transcripts into an LLM, tracking a competitor's channel, or assembling a dataset for fine-tuning. Before you can fetch a single transcript, you need video IDs—and finding them reliably at scale is where most pipelines break.
The official YouTube Data API v3 can technically do this, but it comes with friction: OAuth 2.0, a Google Cloud project, quota limits of 10,000 units per day where a single search call costs 100 units, and a setup process that takes longer than the actual integration. At 100 units per search call, you burn through your daily free quota with 100 searches.
YouTube Transcriber offers a cleaner path. One Bearer token, one endpoint, and results come back as plain JSON with the metadata you actually need—including whether captions are available.
Why Search Is the Entry Point for Ingestion Pipelines
YouTube is enormous. Over 500 hours of video are uploaded to the platform every minute, and the total library sits at roughly 5.1 billion videos as of 2025. You cannot maintain a manually curated list of video IDs for anything that needs to stay current.
Programmatic search solves this in three ways:
- Discovery: Find new videos on a topic as they appear, without visiting YouTube.
- Filtering: Pull only videos that match your criteria (topic, channel, presence of captions) before committing to more expensive operations.
- Automation: Trigger downstream jobs—transcript fetching, summarization, indexing—directly from search results without human intervention.
The search-then-transcribe pattern is the backbone of most YouTube data pipelines. Get it right and everything downstream becomes a loop.
The Search Endpoint
Endpoint: GET https://getyoutubetranscriber.com/api/v2/search
Authentication: Bearer token in the Authorization header.
Query Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| q | string | No* | Search query. Matches against titles, descriptions, channel names, and tags. Max 200 characters. |
| type | string | No | "video" (default) or "channel" |
| continuation | string | No | Pagination cursor from a previous response |
*Either q or continuation is needed to make a useful call.
A Basic Search Request
curl "https://getyoutubetranscriber.com/api/v2/search?q=machine+learning+tutorial&type=video" \
-H "Authorization: Bearer ytt_your_api_key_here"
import requests
API_KEY = "ytt_your_api_key_here"
BASE_URL = "https://getyoutubetranscriber.com/api/v2"
def search_videos(query: str) -> dict:
resp = requests.get(
f"{BASE_URL}/search",
params={"q": query, "type": "video"},
headers={"Authorization": f"Bearer {API_KEY}"},
)
resp.raise_for_status()
return resp.json()
results = search_videos("machine learning tutorial")
print(results)
const API_KEY = "ytt_your_api_key_here";
async function searchVideos(query) {
const url = new URL("https://getyoutubetranscriber.com/api/v2/search");
url.searchParams.set("q", query);
url.searchParams.set("type", "video");
const res = await fetch(url, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
if (!res.ok) throw new Error(`Search failed: ${res.status}`);
return res.json();
}
searchVideos("machine learning tutorial").then(console.log);
Understanding the Response
A successful call returns a JSON object with a results array and pagination metadata:
{
"results": [
{
"type": "video",
"video_id": "dQw4w9WgXcQ",
"title": "Machine Learning Tutorial for Beginners",
"channel_id": "UCuAXFkgsw1L7xaCfnd5JJOw",
"channel_title": "Tech Academy",
"channel_handle": "@techacademy",
"channel_verified": true,
"length_text": "18:42",
"view_count_text": "4.3M views",
"published_time_text": "2 years ago",
"has_captions": true,
"thumbnails": [
{
"url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg",
"width": 480,
"height": 360
}
]
}
],
"result_count": 20,
"continuation_token": "EqQDEg...",
"has_more": true
}
The fields you'll use most:
video_id: The unique ID to pass downstream for transcripts, metadata, or channel lookups.has_captions: A boolean you can use to skip videos before ever calling the transcript endpoint.channel_idandchannel_handle: Useful for grouping results by source or pivoting to a full channel upload list.continuation_token/has_more: Your pagination handles.
Chaining Search to Transcript Fetching
Search gives you IDs. The transcript endpoint turns IDs into text. Connecting them is the core of any content pipeline.
Here's a complete Python example that searches for a topic, filters for captioned videos, and fetches transcripts for the first five results:
import requests
import time
API_KEY = "ytt_your_api_key_here"
BASE_URL = "https://getyoutubetranscriber.com/api/v2"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
def search_videos(query: str) -> list[dict]:
resp = requests.get(
f"{BASE_URL}/search",
params={"q": query, "type": "video"},
headers=HEADERS,
)
resp.raise_for_status()
data = resp.json()
return [r for r in data["results"] if r.get("has_captions")]
def get_transcript(video_id: str) -> dict:
resp = requests.get(
f"{BASE_URL}/transcript",
params={"video_id": video_id, "language": "en"},
headers=HEADERS,
)
resp.raise_for_status()
return resp.json()
def ingest(query: str, limit: int = 5) -> list[dict]:
videos = search_videos(query)[:limit]
corpus = []
for video in videos:
vid = video["video_id"]
try:
transcript = get_transcript(vid)
corpus.append({"video_id": vid, "title": video["title"], "transcript": transcript})
print(f"Fetched: {video['title']}")
except requests.HTTPError as e:
print(f"Skipped {vid}: {e}")
time.sleep(0.5) # be courteous with your credit balance
return corpus
corpus = ingest("python async programming")
The same chain works in JavaScript. For bulk processing where you're pulling hundreds of videos, see the guide on downloading YouTube transcripts in bulk without getting blocked for rate-limiting patterns and retry logic.
Pagination and Result Quality Tips

Walking Through Pages
Each response includes has_more and continuation_token. When has_more is true, pass continuation_token back as the continuation query parameter to get the next page:
def search_all_pages(query: str, max_pages: int = 5) -> list[dict]:
all_results = []
params = {"q": query, "type": "video"}
for page in range(max_pages):
resp = requests.get(
f"{BASE_URL}/search",
params=params,
headers=HEADERS,
)
resp.raise_for_status()
data = resp.json()
all_results.extend(data["results"])
if not data.get("has_more"):
break
params = {"continuation": data["continuation_token"]} # drop q on subsequent pages
return all_results
Gotcha: Once you have a
continuation_token, drop theqparameter from subsequent requests. Pass onlycontinuation. Including both may produce inconsistent results.
Getting Better Results
A few things that improve the signal-to-noise ratio:
Filter on has_captions first. If you only care about transcribable content, skip everything where has_captions is false. This is cheaper than discovering missing captions after calling the transcript endpoint.
Use specific queries. "python tutorial" returns a much larger and noisier result set than "python asyncio event loop tutorial". The more specific your query, the less post-filtering you need.
Mind the view_count_text and published_time_text. These come back as human-readable strings ("4.3M views", "2 years ago"), not integers or timestamps. If you need to sort or filter numerically, you'll need to parse them yourself. For structured channel data with proper timestamps, the channel uploads endpoint is a better fit.
Cap pagination depth. Search result quality degrades past the first few pages. For topic-based ingestion, three to five pages is usually sufficient. Going deeper increases cost without proportional quality gain.
Handle 408 and 429 gracefully. A 429 means you've hit the rate limit—back off and retry. A 408 is a transient upstream error from YouTube's infrastructure—also safe to retry, typically within a few seconds.
When Search Is Not the Right Tool
Search works well for topic-based discovery. It is not the best fit for every use case:
- You want all videos from a specific channel: Use the channel uploads endpoint instead. Search results from a channel are not guaranteed to be complete. See how these endpoints connect if you're monitoring a YouTube channel for new videos via API.
- You already have video IDs: Skip search entirely and go straight to the transcript endpoint.
- You need structured metadata like exact upload dates or subscriber counts: The official YouTube Data API v3 returns structured fields for that. YouTube Transcriber is optimized for transcript extraction and search discovery, not full metadata enumeration.
Putting It Together: A Real-World Ingestion Loop
Here's a minimal but production-shaped ingestion loop that handles pagination, retries, and deduplication:
import requests
import time
from collections import deque
API_KEY = "ytt_your_api_key_here"
BASE_URL = "https://getyoutubetranscriber.com/api/v2"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
def run_pipeline(query: str, max_pages: int = 3):
seen_ids = set()
params = {"q": query, "type": "video"}
for _ in range(max_pages):
for attempt in range(3):
resp = requests.get(f"{BASE_URL}/search", params=params, headers=HEADERS)
if resp.status_code in (408, 429):
time.sleep(2 ** attempt)
continue
resp.raise_for_status()
break
data = resp.json()
for video in data["results"]:
vid = video["video_id"]
if vid in seen_ids or not video.get("has_captions"):
continue
seen_ids.add(vid)
try:
tx = requests.get(
f"{BASE_URL}/transcript",
params={"video_id": vid, "language": "en"},
headers=HEADERS,
)
tx.raise_for_status()
yield {"id": vid, "title": video["title"], "transcript": tx.json()}
except requests.HTTPError:
pass # skip unavailable transcripts
if not data.get("has_more"):
break
params = {"continuation": data["continuation_token"]}
time.sleep(0.3)
for doc in run_pipeline("llm fine-tuning dataset"):
print(doc["title"])
# hand off to your indexer, vector store, or file sink
If you're building this into an AI context, the resulting transcripts slot directly into a RAG pipeline. The pattern is covered in detail in the guide on building a RAG pipeline on YouTube transcripts.
Next Steps
The search endpoint is live and available on the free tier—100 credits, no credit card. The full API reference covers the complete parameter list, response schema, and error codes. If you're already past search and need transcript fetching, bulk processing, or channel monitoring, those are one endpoint away from here.