# YouTube Transcript API — Complete Documentation > Enterprise-grade YouTube transcript extraction, video metadata, search, and batch processing API. Built for AI agents and LLM pipelines. Base URL: https://api.ytapi.dev Authentication: Bearer token via Authorization header API Version: v1 --- # AI Agent Setup & Tooling The **YouTube Transcript & Data API** is designed AI-native from the ground up. Autonomous agents, LLMs, and IDE assistants can read clean markdown schemas, query machine-readable indexes, and execute tools via the Model Context Protocol (MCP). *** Machine-Readable Endpoints [#machine-readable-endpoints] For LLMs and agents that need to fetch documentation into context windows without parsing heavy HTML: | Resource | URL | Description | | :----------------- | :------------------------------------- | :---------------------------------------------------------------------------------- | | **LLMs Index** | `https://docs.ytapi.dev/llms.txt` | Spec-compliant manifest of all documentation pages with short descriptions. | | **Full LLMs Dump** | `https://docs.ytapi.dev/llms-full.txt` | Single concatenated Markdown document containing the entire platform documentation. | | **Raw Page MDX** | `https://docs.ytapi.dev/{slug}.mdx` | Append `.mdx` to any documentation URL to retrieve raw, clean Markdown. | In bash-based agents (e.g. Claude Code, Codex, Aider), fetch the entire API reference with: ```bash curl -s https://docs.ytapi.dev/llms-full.txt ``` *** Model Context Protocol (MCP) [#model-context-protocol-mcp]
Connect your AI assistants directly to the YouTube Transcript API using the Model Context Protocol (MCP). Claude Desktop & Cursor Configuration [#claude-desktop--cursor-configuration] Add the following to your MCP client configuration (`claude_desktop_config.json` or `.cursor/mcp.json`): ```json { "mcpServers": { "youtube-transcript": { "command": "npx", "args": ["-y", "@ytapi/mcp-server"], "env": { "YT_API_KEY": "YOUR_API_KEY" } } } } ``` Available MCP Tools [#available-mcp-tools] | Tool Name | Parameters | Description | | :--------------- | :----------------------------------- | :----------------------------------------------------------------------- | | `get_transcript` | `video_id`, `format`, `languages` | Fetch transcript in `markdown`, `srt`, `vtt`, or `word_timestamps`. | | `get_video_info` | `video_id`, `mode` (`fast` / `full`) | Fetch metadata, author, view counts, and chapter boundaries. | | `search_youtube` | `query`, `type`, `limit` | Full-text video and channel search without consuming Google Cloud quota. | | `get_comments` | `video_id`, `sort_by`, `limit` | Retrieve top or recent comment threads with engagement metrics. | *** Code Mode & Function Calling [#code-mode--function-calling]
When orchestrating extraction pipelines via OpenAI, Anthropic, or Gemini tool calling, provide the JSON Schema directly: ```json { "name": "fetch_youtube_transcript", "description": "Extract subtitles or transcripts from any YouTube video in structured Markdown or SRT.", "parameters": { "type": "object", "properties": { "video_id": { "type": "string", "description": "11-character YouTube video ID or full URL" }, "format": { "type": "string", "enum": ["markdown", "text", "srt", "vtt", "word_timestamps"], "default": "markdown" } }, "required": ["video_id"] } } ``` *** Platform Skills & System Prompts [#platform-skills--system-prompts]
Cursor Rules (.cursorrules) [#cursor-rules-cursorrules] Add this prompt rule to your project to instruct Cursor on how to query YouTube transcripts: ```markdown # YouTube Transcript API Guidelines When writing code that extracts YouTube subtitles or transcripts: 1. Always use `https://api.ytapi.dev/v1/transcripts` with `Authorization: Bearer $YT_API_KEY`. 2. For LLM summaries or context injection, specify `"format": "markdown"`. 3. For video subtitle synchronizing, specify `"format": "word_timestamps"` with `"word_level": true`. 4. Check `X-Cache` response headers (`HIT` or `MISS`) to measure latency. 5. Refer to complete documentation at `https://docs.ytapi.dev/llms.txt`. ``` --- # Authentication & Security All requests to the YouTube Transcript Platform API must be authenticated using a valid API key. Bearer Token Authentication [#bearer-token-authentication] Include your API key as a Bearer token in the `Authorization` header: ```http Authorization: Bearer YOUR_API_KEY ``` Never expose your API key in client-side code, public repositories, or browser network requests. Use environment variables and server-side proxying. Requests without a valid `Authorization` header receive a `401 Unauthorized` response: ```json { "error": "Unauthorized: invalid or missing API key", "status": 401 } ``` *** API Key Scopes [#api-key-scopes] Keys can be generated with specific scopes from the [Developer Dashboard](https://ytapi.dev/app/api-keys): | Scope | Description | | :------------------- | :----------------------------------------------------------------------- | | `transcripts:read` | Extract transcripts in all formats, word-level offsets, and AI Markdown. | | `transcripts:stream` | Stream real-time transcript chunks via SSE / WebSocket. | | `admin` | Full programmatic access and team management. | *** Rate Limits & Concurrency [#rate-limits--concurrency] API keys have default rate limits based on your account tier: | Account | Rate Limit | Burst | | :------------------ | :----------------- | :----------------- | | **Free** | 30 RPM (0.5 req/s) | 2 | | **Paid** (any pack) | 300 RPM (5 req/s) | 10 | | **Reserved** | Above 300 RPM | Dedicated door set | When you exceed your rate limit, the API returns `429 Too Many Requests` with a `Retry-After` header indicating seconds until reset. See [Credits & Errors](/credits) for all error codes. --- # Credits, Rate Limits & Errors The platform meters credits per successful billed response. Failed calls are free. Credits never expire. Most endpoints cost 1 credit; video `mode=full` costs 2. See [Pricing](/pricing) for packs. Rate limits [#rate-limits] | Account | Rate limit | | :--------------------- | :----------------- | | Free | 30 RPM (0.5 req/s) | | Paid (any credit pack) | 300 RPM (5 req/s) | | Reserved | Contact us | Customer `Cache-Control: no-cache` is ignored. Cache policy is server-side. Credit Schedule [#credit-schedule] | API Feature | Endpoint | Cost | | :--------------------------- | :---------------------------------------------------------- | :----------------------------------- | | Standard Transcript | [`POST /v1/transcripts`](/transcripts/extract) | | | AI-Native Markdown | [`POST /v1/transcripts`](/transcripts/extract) | | | Video metadata (`mode=fast`) | [`GET /v1/videos/{id}`](/videos/get) | | | Video metadata (`mode=full`) | [`GET /v1/videos/{id}?mode=full`](/videos/get#returns-full) | | | Playlist Details | [`GET /v1/playlists/{id}`](/playlists/get) | | | Playlist Videos | [`GET /v1/playlists/{id}/videos`](/playlists/videos) | | | Channel Profile | [`GET /v1/channels/{id}`](/channels/get) | | | Channel Streams | [`GET /v1/channels/{id}/streams`](/channels/streams) | | | Channel Playlists | [`GET /v1/channels/{id}/playlists`](/channels/playlists) | | | Search | [`GET /v1/search`](/search/query) | | | Search Autocomplete | [`GET /v1/search/suggestions`](/search/suggestions) | | | Batch Processing | [`POST /v1/batch`](/batch/run) | per task | *** Error Response Format [#error-response-format] All errors return a standard JSON envelope: ```json { "error": "Transcripts are disabled or unavailable for this video", "status": 404 } ``` *** HTTP Status Codes [#http-status-codes] Missing required parameters (e.g. invalid video ID or empty search query). Check your request payload. Missing or invalid API key in `Authorization: Bearer `. Verify your key in the [Dashboard](https://ytapi.dev/app/api-keys). Credit balance depleted. Buy a pack in the [billing dashboard](https://ytapi.dev/app/billing). Failed requests are still free. Video does not exist, is private, or captions are unavailable for the requested language. Rate limit exceeded. Check the `Retry-After` header for seconds until reset. See [Authentication](/authentication) for tier limits. Upstream extraction error. Credits are **automatically refunded** for failed requests. --- # Platform Overview & Quickstart Welcome to the **YouTube Transcript & Data API** — the fastest way to extract transcripts, metadata, and engagement data from YouTube. Purpose-built for AI agents, LLM pipelines, video intelligence, and content indexing. All API endpoints are versioned under the `/v1` prefix. **Production:** `https://api.ytapi.dev/v1` **Direct Node:** `https://cx33.ytapi.dev/v1` Explore the API [#explore-the-api] *** 30-Second Quickstart [#30-second-quickstart] Get Your API Key [#get-your-api-key] Generate an API key with `transcripts:read` permissions from the [Developer Dashboard](https://ytapi.dev/app/api-keys). Make Your First Request [#make-your-first-request] ```bash curl -X POST https://api.ytapi.dev/v1/transcripts \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "video_id": "dQw4w9WgXcQ", "format": "markdown" }' ``` ```python import requests response = requests.post( "https://api.ytapi.dev/v1/transcripts", headers={"Authorization": "Bearer YOUR_API_KEY"}, json={ "video_id": "dQw4w9WgXcQ", "format": "markdown", "word_level": False } ) print(response.json()) ``` ```typescript const res = await fetch("https://api.ytapi.dev/v1/transcripts", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ video_id: "dQw4w9WgXcQ", format: "markdown" }) }); const data = await res.json(); console.log(data); ``` ```go package main import ( "bytes" "encoding/json" "fmt" "net/http" ) func main() { payload, _ := json.Marshal(map[string]any{ "video_id": "dQw4w9WgXcQ", "format": "markdown", }) req, _ := http.NewRequest("POST", "https://api.ytapi.dev/v1/transcripts", bytes.NewBuffer(payload)) req.Header.Set("Authorization", "Bearer YOUR_API_KEY") req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() fmt.Println("Status:", resp.Status) } ``` Parse the Response [#parse-the-response] Every response includes telemetry headers for observability: | Header | Description | | :-------------------- | :---------------------------------------------------- | | `X-Cache` | `HIT` or `MISS` — whether served from cache | | `X-Cache-Tier` | `L1_MEMORY` (sub-10ms) or `L2_PERSISTENT` | | `X-Track-Type` | `ASR` (auto-generated) or `MANUAL` (creator-uploaded) | | `X-Language-Code` | Negotiated ISO language code (e.g. `en`, `zh-Hans`) | | `X-Duration-Seconds` | Extraction latency in seconds | | `X-Credits-To-Deduct` | Credits consumed for this request | *** Core Capabilities [#core-capabilities] | Capability | Highlight | | :------------------------- | :----------------------------------------------------------------------------------------------------------- | | **Wire-Speed Transcripts** | Sub-400ms median extraction with automatic language negotiation and multi-tier edge caching | | **AI-Native Markdown** | `format=markdown` produces structured paragraphs with timestamp references optimized for LLM context windows | | **Word-Level Precision** | `word_level=true` provides millisecond-accurate word offsets for speech sync and video editing | | **Playlists & Series** | Complete playlist details, video listings, and opaque cursor continuation pagination | | **Channels & Streams** | Verified channel profiles, subscriber counts, live/past streams, and created playlists | | **Zero-Quota Search** | Full-text search across YouTube without consuming Google Cloud API quotas | | **Concurrent Batch** | Up to 10 parallel extraction tasks in a single HTTP request | --- # Pricing YTAPI sells **credit packs** that add to one balance. There is no monthly subscription for new accounts. Buy any size, as often as you want. | Credits | Amount | Effective | | ------: | -------------: | :--------- | | 100 | Free on signup | — | | 2,000 | $9 | $4.50 / 1k | | 10,000 | $29 | $2.90 / 1k | | 50,000 | $99 | $1.98 / 1k | * **100 credits** on signup, no card. * Credits **never expire**. * Any paid pack sets every key on the account to **300 RPM (5 req/s)**. Free keys are **30 RPM**. * Need more than 300 RPM? [Contact us](https://ytapi.dev/contact) for reserved doors. Buy packs in the [billing dashboard](https://ytapi.dev/app/billing). See [Credits & errors](/credits) for per-endpoint costs. --- # Batch Reduce round-trips for indexing pipelines. Credits are charged per successful task. Failed tasks consume 0 credits. Run batch --- # Run batch Each task runs in isolation. The batch itself does not fail as a whole: inspect `success` on every result. You are billed **1 credit per successful task**. Up to 10 task objects. Caller-defined id echoed on the result. `transcript` or `video_info`. 11-character YouTube video ID. Transcript format when `type` is `transcript`. Video lookup mode when `type` is `video_info` (`fast` or `full`). Number of tasks submitted. Tasks with `"success": true`. Isolated failures (missing captions, invalid ids). Wall-clock time for the whole batch. Per-task outcome, including `id`, `video_id`, `type`, `success`, `latency_ms`, and `data`. Only tasks with `"success": true` consume credits. ```ts const response = await fetch("https://api.ytapi.dev/v1/batch", { method: "POST", headers: { Authorization: `Bearer ${process.env.YT_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ tasks: [ { id: "task_1", type: "transcript", video_id: "dQw4w9WgXcQ", format: "markdown", }, { id: "task_2", type: "video_info", video_id: "jNQXAC9IVRw", mode: "fast", }, ], }), }); const batch = await response.json(); for (const result of batch.results) { console.log(result.id, result.success); } ``` ```python import os import requests response = requests.post( "https://api.ytapi.dev/v1/batch", headers={"Authorization": f"Bearer {os.environ['YT_API_KEY']}"}, json={ "tasks": [ {"id": "1", "type": "transcript", "video_id": "dQw4w9WgXcQ", "format": "segments"}, {"id": "2", "type": "transcript", "video_id": "jNQXAC9IVRw", "format": "markdown"}, ] }, ) results = response.json() for item in results["results"]: print(item["id"], item["success"]) ``` ```bash curl -X POST https://api.ytapi.dev/v1/batch \ -H "Authorization: Bearer $YT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "tasks": [ { "id": "task_1", "type": "transcript", "video_id": "dQw4w9WgXcQ", "format": "markdown" }, { "id": "task_2", "type": "video_info", "video_id": "jNQXAC9IVRw", "mode": "fast" } ] }' ``` ```go package main import ( "bytes" "encoding/json" "fmt" "net/http" "os" ) func main() { payload, _ := json.Marshal(map[string]any{ "tasks": []map[string]any{ {"id": "task_1", "type": "transcript", "video_id": "dQw4w9WgXcQ", "format": "markdown"}, {"id": "task_2", "type": "video_info", "video_id": "jNQXAC9IVRw", "mode": "fast"}, }, }) req, _ := http.NewRequest("POST", "https://api.ytapi.dev/v1/batch", bytes.NewBuffer(payload)) req.Header.Set("Authorization", "Bearer "+os.Getenv("YT_API_KEY")) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() fmt.Println(resp.Status) } ``` ```json { "total_tasks": 2, "successful_tasks": 2, "failed_tasks": 0, "total_latency_ms": 348, "results": [ { "id": "task_1", "video_id": "dQw4w9WgXcQ", "type": "transcript", "success": true, "latency_ms": 312, "data": { "format": "markdown", "markdown": "# Never Gonna Give You Up\n\n[00:00] We're no strangers to love..." } }, { "id": "task_2", "video_id": "jNQXAC9IVRw", "type": "video_info", "success": true, "latency_ms": 84, "data": { "title": "Me at the zoo", "author": "jawed", "duration_seconds": 19 } } ] } ``` --- # Get channel Handles such as `@MrBeast` and 24-character channel IDs (`UC...`) are resolved automatically. Channel handle (for example `@MrBeast`) or canonical channel ID. ISO country hint used when region availability matters. Canonical 24-character channel ID. Channel display name. Public handle, including `@`. Channel about text. Approximate subscriber count. Public upload count. Whether the channel has a verified badge. Avatar images. Banner images. External links from the channel page. Tabs that can be listed, such as `videos`, `shorts`, `streams`, and `playlists`. ```ts const response = await fetch("https://api.ytapi.dev/v1/channels/@MrBeast", { headers: { Authorization: `Bearer ${process.env.YT_API_KEY}`, }, }); const channel = await response.json(); console.log(channel.title, channel.subscriber_count); ``` ```python import os import requests response = requests.get( "https://api.ytapi.dev/v1/channels/@MrBeast", headers={"Authorization": f"Bearer {os.environ['YT_API_KEY']}"}, ) print(response.json()) ``` ```bash curl -X GET "https://api.ytapi.dev/v1/channels/@MrBeast" \ -H "Authorization: Bearer $YT_API_KEY" ``` ```go package main import ( "fmt" "net/http" "os" ) func main() { req, _ := http.NewRequest("GET", "https://api.ytapi.dev/v1/channels/@MrBeast", nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("YT_API_KEY")) resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() fmt.Println(resp.Status) } ``` ```json { "channel_id": "UCX6OQ3DkcsbYNE6H8uQQuVA", "title": "MrBeast", "handle": "@MrBeast", "description": "SUBSCRIBE FOR A COOKIE! New video every single Saturday...", "subscriber_count": 517000000, "subscriber_count_text": "517M subscribers", "video_count": 1000, "verified": true, "custom_url": "http://www.youtube.com/@MrBeast", "country": "US", "thumbnails": [ { "url": "https://yt3.googleusercontent.com/nxYrc_1_=s900", "width": 900, "height": 900 } ], "links": ["https://www.instagram.com/mrbeast/"], "available_tabs": ["videos", "shorts", "streams", "playlists"] } ``` --- # Channels Supports canonical channel IDs (`UC...`) and handles (`@handle`). Get channel List channel videos List channel streams List channel playlists --- # List channel playlists Returns public playlists for the channel. Pass `next_cursor` as `cursor` to continue. Channel handle or canonical channel ID. Continuation cursor from the previous response. Playlist cards with `playlist_id`, `title`, `video_count`, and thumbnails. Whether another page is available. Cursor for the next page. ```ts const id = "UCLA_DiR1FfKNvjuUpBHmylQ"; const response = await fetch( `https://api.ytapi.dev/v1/channels/${id}/playlists`, { headers: { Authorization: `Bearer ${process.env.YT_API_KEY}`, }, }, ); const page = await response.json(); console.log(page.playlists[0]?.title, page.next_cursor); ``` ```python import os import requests channel_id = "UCLA_DiR1FfKNvjuUpBHmylQ" response = requests.get( f"https://api.ytapi.dev/v1/channels/{channel_id}/playlists", headers={"Authorization": f"Bearer {os.environ['YT_API_KEY']}"}, ) print(response.json()) ``` ```bash curl -X GET "https://api.ytapi.dev/v1/channels/UCLA_DiR1FfKNvjuUpBHmylQ/playlists" \ -H "Authorization: Bearer $YT_API_KEY" ``` ```go package main import ( "fmt" "net/http" "os" ) func main() { url := "https://api.ytapi.dev/v1/channels/UCLA_DiR1FfKNvjuUpBHmylQ/playlists" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("YT_API_KEY")) resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() fmt.Println(resp.Status) } ``` ```json { "playlists": [ { "playlist_id": "PL2aBZuCeDwlSTqO83s67bXvSDEq_gB1PZ", "title": "Artemis: Back to the Moon", "video_count": 48, "video_count_text": "48 videos", "thumbnails": [ { "url": "https://i.ytimg.com/vi/6gMEMYh5HL8/hqdefault.jpg", "width": 480, "height": 360 } ] } ], "has_more": true, "next_cursor": "yt_7c0e86b240fe81347072" } ``` --- # List channel streams Lists the channel **Live** tab: current broadcasts and past streams. Uploads live on [`List channel videos`](/channels/videos). The two lists are different YouTube tabs and do not substitute for each other. Page with the returned `next_cursor`. Channel handle or canonical channel ID. Continuation cursor from the previous response. Canonical channel ID. Stream items for this page. Same video card shape as channel uploads. Whether another page is available. Cursor for the next page. ```ts const response = await fetch( "https://api.ytapi.dev/v1/channels/@MrBeast/streams", { headers: { Authorization: `Bearer ${process.env.YT_API_KEY}`, }, }, ); const page = await response.json(); console.log(page.videos.length, page.next_cursor); ``` ```python import os import requests response = requests.get( "https://api.ytapi.dev/v1/channels/@MrBeast/streams", headers={"Authorization": f"Bearer {os.environ['YT_API_KEY']}"}, ) print(response.json()) ``` ```bash curl -X GET "https://api.ytapi.dev/v1/channels/@MrBeast/streams" \ -H "Authorization: Bearer $YT_API_KEY" ``` ```go package main import ( "fmt" "net/http" "os" ) func main() { req, _ := http.NewRequest("GET", "https://api.ytapi.dev/v1/channels/@MrBeast/streams", nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("YT_API_KEY")) resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() fmt.Println(resp.Status) } ``` ```json { "channel_id": "UCX6OQ3DkcsbYNE6H8uQQuVA", "has_more": true, "next_cursor": "yt_7c0e86b240fe81347072", "videos": [ { "video_id": "abcLiveStream", "title": "Live: $1 vs $1,000,000 Hotel", "length_text": "LIVE", "thumbnails": [ { "url": "https://i.ytimg.com/vi/abcLiveStream/hqdefault.jpg", "width": 480, "height": 360 } ] } ] } ``` --- # List channel videos Lists public uploads. Pass `continuation` or `cursor` from the previous page to keep paging. Channel handle or canonical channel ID. `newest` (default), `popular`, or `oldest`. Continuation token from the previous response. Alias for `continuation`. Canonical channel ID. Channel display name. This page of uploads, including `video_id`, `title`, thumbnails, and length. Whether another page is available. Cursor for the next page. Alias of `next_cursor`. ```ts const response = await fetch( "https://api.ytapi.dev/v1/channels/@MrBeast/videos?sort_by=popular", { headers: { Authorization: `Bearer ${process.env.YT_API_KEY}`, }, }, ); const page = await response.json(); console.log(page.videos[0]?.title, page.next_cursor); ``` ```python import os import requests response = requests.get( "https://api.ytapi.dev/v1/channels/@MrBeast/videos", headers={"Authorization": f"Bearer {os.environ['YT_API_KEY']}"}, params={"sort_by": "popular"}, ) print(response.json()) ``` ```bash curl -X GET "https://api.ytapi.dev/v1/channels/@MrBeast/videos?sort_by=popular" \ -H "Authorization: Bearer $YT_API_KEY" ``` ```go package main import ( "fmt" "net/http" "os" ) func main() { url := "https://api.ytapi.dev/v1/channels/@MrBeast/videos?sort_by=popular" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("YT_API_KEY")) resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() fmt.Println(resp.Status) } ``` ```json { "channel_id": "UCX6OQ3DkcsbYNE6H8uQQuVA", "channel_title": "MrBeast", "has_more": true, "next_cursor": "yt_7c0e86b240fe81347072", "continuation": "yt_7c0e86b240fe81347072", "videos": [ { "video_id": "0e3GPea1Tyg", "title": "$456,000 Squid Game In Real Life!", "length_text": "25:42", "thumbnails": [ { "url": "https://i.ytimg.com/vi/0e3GPea1Tyg/hq720.jpg", "width": 720, "height": 404 } ] } ] } ``` --- # Get playlist Returns playlist overview plus the initial page of video items. Use `next_cursor` with [List playlist videos](/playlists/videos) for later pages. Playlist ID (`PL...`) or a full YouTube playlist URL. ISO country hint used when region availability matters. Canonical playlist ID. Playlist title. Playlist description. Total videos in the playlist. Display string for views. Owner channel name. Artwork at one or more sizes. First page of items, including `video_id`, `title`, `index`, and duration. Whether another page is available. Opaque cursor for the next page. ```ts const id = "PL-_QwjtlyjHZRRt4AQnQtYFxbiVP-oyuF"; const response = await fetch(`https://api.ytapi.dev/v1/playlists/${id}`, { headers: { Authorization: `Bearer ${process.env.YT_API_KEY}`, }, }); const playlist = await response.json(); console.log(playlist.title, playlist.next_cursor); ``` ```python import os import requests playlist_id = "PL-_QwjtlyjHZRRt4AQnQtYFxbiVP-oyuF" response = requests.get( f"https://api.ytapi.dev/v1/playlists/{playlist_id}", headers={"Authorization": f"Bearer {os.environ['YT_API_KEY']}"}, ) print(response.json()) ``` ```bash curl -X GET "https://api.ytapi.dev/v1/playlists/PL-_QwjtlyjHZRRt4AQnQtYFxbiVP-oyuF" \ -H "Authorization: Bearer $YT_API_KEY" ``` ```go package main import ( "fmt" "net/http" "os" ) func main() { url := "https://api.ytapi.dev/v1/playlists/PL-_QwjtlyjHZRRt4AQnQtYFxbiVP-oyuF" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("YT_API_KEY")) resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() fmt.Println(resp.Status) } ``` ```json { "playlist_id": "PL-_QwjtlyjHZRRt4AQnQtYFxbiVP-oyuF", "title": "Pomodoro 25-5 / SWM", "description": "Study with me 25-5 pomodoro sessions and lofi beats.", "video_count": 94, "view_count_text": "25,616 views", "author": "Carrot TD", "thumbnails": [ { "url": "https://i.ytimg.com/vi/6gMEMYh5HL8/hqdefault.jpg", "width": 480, "height": 360 } ], "videos": [ { "video_id": "6gMEMYh5HL8", "title": "3-HOUR STUDY WITH ME / calm lofi / Pomodoro 25-5", "index": 1, "length_seconds": 10800, "length_text": "3:00:00", "author": "Carrot TD" } ], "has_more": true, "next_cursor": "yt_e48bb6856a23e4e42efc" } ``` --- # Playlists Retrieve playlist metadata and video catalogs. Raw playlist IDs (`PL...`, `OLAK5uy_...`), custom URLs, and full YouTube playlist links are accepted. Get playlist List playlist videos --- # List playlist videos Page through a playlist using the `next_cursor` returned by [Get playlist](/playlists/get) or a previous call to this endpoint. Playlist ID (`PL...`) or a full YouTube playlist URL. Opaque continuation cursor from the previous response. ISO country hint used when region availability matters. Canonical playlist ID. Playlist title when included. This page of items. Whether another page is available. Cursor for the next page, or `null` when complete. ```ts const id = "PL-_QwjtlyjHZRRt4AQnQtYFxbiVP-oyuF"; const cursor = "yt_e48bb6856a23e4e42efc"; const response = await fetch( `https://api.ytapi.dev/v1/playlists/${id}/videos?cursor=${cursor}`, { headers: { Authorization: `Bearer ${process.env.YT_API_KEY}`, }, }, ); const page = await response.json(); console.log(page.videos.length, page.has_more); ``` ```python import os import requests playlist_id = "PL-_QwjtlyjHZRRt4AQnQtYFxbiVP-oyuF" response = requests.get( f"https://api.ytapi.dev/v1/playlists/{playlist_id}/videos", headers={"Authorization": f"Bearer {os.environ['YT_API_KEY']}"}, params={"cursor": "yt_e48bb6856a23e4e42efc"}, ) print(response.json()) ``` ```bash curl -X GET "https://api.ytapi.dev/v1/playlists/PL-_QwjtlyjHZRRt4AQnQtYFxbiVP-oyuF/videos?cursor=yt_e48bb6856a23e4e42efc" \ -H "Authorization: Bearer $YT_API_KEY" ``` ```go package main import ( "fmt" "net/http" "os" ) func main() { url := "https://api.ytapi.dev/v1/playlists/PL-_QwjtlyjHZRRt4AQnQtYFxbiVP-oyuF/videos?cursor=yt_e48bb6856a23e4e42efc" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("YT_API_KEY")) resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() fmt.Println(resp.Status) } ``` ```json { "playlist_id": "PL-_QwjtlyjHZRRt4AQnQtYFxbiVP-oyuF", "title": "Pomodoro 25-5 / SWM", "videos": [ { "video_id": "kd1m7m7AAhw", "title": "3-HOUR STUDY WITH ME / Late night / Pomodoro 25-5", "index": 82, "length_seconds": 10662, "length_text": "2:57:42", "author": "Carrot TD" } ], "has_more": false, "next_cursor": null } ``` --- # Search Query YouTube without consuming Google Cloud YouTube Data API v3 units. Search Get suggestions --- # Search YouTube Search videos, channels, playlists, or movies. Does not consume YouTube Data API quota. Search keyword or phrase. Max results to return, from 1 to 50. Default `20`. `video`, `channel`, `playlist`, or `movie`. Default `video`. `hour`, `today`, `week`, `month`, or `year`. `relevance`, `date`, `view_count`, or `rating`. Default `relevance`. Echoed search string. Number of items in this response. Hits with `id`, `type`, `title`, channel fields, duration, views, and thumbnail. ```ts const params = new URLSearchParams({ q: "AI Agents", type: "video", upload_date: "month", limit: "10", }); const response = await fetch(`https://api.ytapi.dev/v1/search?${params}`, { headers: { Authorization: `Bearer ${process.env.YT_API_KEY}`, }, }); const data = await response.json(); console.log(data.results.map((item: { title: string }) => item.title)); ``` ```python import os import requests response = requests.get( "https://api.ytapi.dev/v1/search", headers={"Authorization": f"Bearer {os.environ['YT_API_KEY']}"}, params={ "q": "AI Agents", "type": "video", "upload_date": "month", "limit": 10, }, ) print(response.json()) ``` ```bash curl -X GET "https://api.ytapi.dev/v1/search?q=AI+Agents&type=video&upload_date=month&limit=10" \ -H "Authorization: Bearer $YT_API_KEY" ``` ```go package main import ( "fmt" "net/http" "os" ) func main() { url := "https://api.ytapi.dev/v1/search?q=AI+Agents&type=video&upload_date=month&limit=10" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("YT_API_KEY")) resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() fmt.Println(resp.Status) } ``` ```json { "query": "AI Agents", "total_results": 10, "results": [ { "id": "abc123xyz89", "type": "video", "title": "Building Autonomous AI Coding Agents from Scratch", "channel_title": "AI Engineering Hub", "channel_id": "UC1234567890", "duration_seconds": 1240, "view_count": 89420, "upload_date": "2024-03-10", "thumbnail_url": "https://i.ytimg.com/vi/abc123xyz89/mqdefault.jpg" } ] } ``` --- # Get suggestions Returns autocomplete strings for a partial query. Use this for search-as-you-type UIs. This endpoint is free. Partial query string. Echoed input. Ranked completion strings. Search suggestions consume **0 credits**. ```ts const q = encodeURIComponent("nextjs 1"); const response = await fetch( `https://api.ytapi.dev/v1/search/suggestions?q=${q}`, { headers: { Authorization: `Bearer ${process.env.YT_API_KEY}`, }, }, ); const data = await response.json(); console.log(data.suggestions); ``` ```python import os import requests response = requests.get( "https://api.ytapi.dev/v1/search/suggestions", headers={"Authorization": f"Bearer {os.environ['YT_API_KEY']}"}, params={"q": "nextjs 1"}, ) print(response.json()) ``` ```bash curl -X GET "https://api.ytapi.dev/v1/search/suggestions?q=nextjs+1" \ -H "Authorization: Bearer $YT_API_KEY" ``` ```go package main import ( "fmt" "net/http" "os" ) func main() { url := "https://api.ytapi.dev/v1/search/suggestions?q=nextjs+1" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("YT_API_KEY")) resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() fmt.Println(resp.Status) } ``` ```json { "query": "nextjs 1", "suggestions": [ "nextjs 15 tutorial", "nextjs 15 crash course", "nextjs 15 server actions", "nextjs 15 auth", "nextjs 15 cache components" ] } ``` --- # Extract transcript This is the primary transcript endpoint. Submit a JSON body with the video id and optional format, language, and track policy. `GET /v1/transcripts` runs the same extractor with query parameters. Successful requests consume 1 credit. Missing captions return `404` and are not billed. 11-character YouTube video ID or a full video URL. Output format: `word_timestamps`, `markdown`, `segments`, `sentences`, `text`, `srt`, `vtt`, `json3`, or `words`. Default `word_timestamps`. Include millisecond word offsets. Only genuine ASR word-level tracks populate `words`. Default `false`. Prioritized ISO language codes, for example `["en", "es"]`. Default `["auto"]`. Track selection: `manual_first`, `asr_first`, `manual_only`, or `asr_only`. Default `manual_first`. Max upstream processing deadline in milliseconds before fallback. Default `10000`. Echoed YouTube video ID. Negotiated ISO language code, for example `en` or `zh-Hans`. `ASR` (auto-generated) or `MANUAL` (creator-uploaded). Format actually returned. Payload shape depends on `format`. Markdown responses include token estimates for LLM windows. Structured paragraphs with timestamp references. Present when `format` is `markdown`. Approximate token count of the markdown payload. Timed caption segments. Each item may include a `words` array when `word_level` is true on ASR tracks. If a video only has manual subtitles without word-level alignment, the API returns segments without fabricating a `words` array. ```ts const response = await fetch( "https://api.ytapi.dev/v1/transcripts", { method: "POST", headers: { Authorization: `Bearer ${process.env.YT_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ video_id: "dQw4w9WgXcQ", format: "markdown", }), }, ); const transcript = await response.json(); console.log(transcript); ``` ```python import os import requests response = requests.post( "https://api.ytapi.dev/v1/transcripts", headers={"Authorization": f"Bearer {os.environ['YT_API_KEY']}"}, json={ "video_id": "dQw4w9WgXcQ", "format": "markdown", "word_level": False, }, ) print(response.json()) ``` ```bash curl -X POST https://api.ytapi.dev/v1/transcripts \ -H "Authorization: Bearer $YT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "video_id": "dQw4w9WgXcQ", "format": "markdown" }' ``` ```go package main import ( "bytes" "encoding/json" "fmt" "net/http" "os" ) func main() { payload, _ := json.Marshal(map[string]any{ "video_id": "dQw4w9WgXcQ", "format": "markdown", }) req, _ := http.NewRequest("POST", "https://api.ytapi.dev/v1/transcripts", bytes.NewBuffer(payload)) req.Header.Set("Authorization", "Bearer "+os.Getenv("YT_API_KEY")) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() fmt.Println(resp.Status) } ``` ```json { "video_id": "dQw4w9WgXcQ", "language": "en", "track_type": "ASR", "format": "markdown", "data": { "markdown": "# Never Gonna Give You Up\n\n[00:00] We're no strangers to love...", "estimated_tokens": 420 } } ``` --- # Get transcript Same extraction as `POST /v1/transcripts`, using query parameters. Useful for browser checks and short scripts. Prefer POST when sending `languages` arrays or larger option sets. 11-character YouTube video ID or a full video URL. Output format. Same values as the POST endpoint. Default `word_timestamps`. Include millisecond word offsets on ASR tracks. Default `false`. Comma-separated ISO language codes, for example `en,es`. `manual_first`, `asr_first`, `manual_only`, or `asr_only`. Default `manual_first`. Echoed YouTube video ID. Negotiated ISO language code. `ASR` or `MANUAL`. Format actually returned. Payload shape depends on `format`. See [Extract transcript](/transcripts/extract#returns). ```ts const params = new URLSearchParams({ video_id: "dQw4w9WgXcQ", format: "markdown", }); const response = await fetch( `https://api.ytapi.dev/v1/transcripts?${params}`, { headers: { Authorization: `Bearer ${process.env.YT_API_KEY}`, }, }, ); const transcript = await response.json(); console.log(transcript); ``` ```python import os import requests response = requests.get( "https://api.ytapi.dev/v1/transcripts", headers={"Authorization": f"Bearer {os.environ['YT_API_KEY']}"}, params={"video_id": "dQw4w9WgXcQ", "format": "markdown"}, ) print(response.json()) ``` ```bash curl -X GET "https://api.ytapi.dev/v1/transcripts?video_id=dQw4w9WgXcQ&format=markdown" \ -H "Authorization: Bearer $YT_API_KEY" ``` ```go package main import ( "fmt" "net/http" "os" ) func main() { url := "https://api.ytapi.dev/v1/transcripts?video_id=dQw4w9WgXcQ&format=markdown" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("YT_API_KEY")) resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() fmt.Println(resp.Status) } ``` ```json { "video_id": "dQw4w9WgXcQ", "language": "en", "track_type": "ASR", "format": "markdown", "data": { "markdown": "# Never Gonna Give You Up\n\n[00:00] We're no strangers to love...", "estimated_tokens": 420 } } ``` --- # Transcripts Extract captions and subtitle transcripts from any public or unlisted YouTube video. Open an endpoint from the sidebar, or start here: Extract transcript Get transcript `POST` is recommended for structured JSON payloads. `GET` is convenient for browser testing and quick scripts. --- # Get video Two lookup depths share this path. `mode=fast` (default) reads the player response only: title, views, duration, thumbnails, caption languages, and basic channel attribution. Typically under a second, **1 credit**. `mode=full` also fetches the watch-next payload: like count, chapters, engagement heatmap, publish time, subscriber text, and channel avatar. **2 credits**. Cache keys are separate, so a fast hit does not satisfy a full request. Successful `mode=fast` responses deduct 1 credit. Successful `mode=full` responses deduct 2 credits. Failed lookups are not billed. 11-character YouTube video ID. `fast` (default) or `full`. ISO country hint used when region availability matters. Alias: `country`. Echoed YouTube video ID. Video title. Channel display name. Same value as `channel.title`. Canonical channel ID. Same value as `channel.id`. Channel attribution from the player response. Canonical channel ID. Channel display name. Channel URL. Player description / short description. Duration in seconds. View count at request time. Creator tags when the player returns them. Player thumbnail ladder. Image URL. Width in pixels. Height in pixels. Caption tracks advertised on the player. Present in both modes. ISO language code, for example `en`. Player display name. `asr` or `manual`. Track variant when YouTube exposes one, for example `gemini`. Whether YouTube marks the track as translatable. Echoed lookup mode. `fast` in this response. All `mode=fast` fields, plus: Unix timestamp (seconds) for the upload / publish date. Like count from the watch page. Subscriber text from the watch page, for example `1.69M subscribers`. Best channel avatar URL from the watch page. Timestamped chapters when YouTube exposes them (overlay, description, or pinned comment). Omitted when none exist. Chapter title. Start offset in seconds. Display timestamp, for example `0:00`. Chapter thumbnail ladder when present. Most-replayed waveform from the watch page. Omitted when YouTube does not provide one. Normalized intensity samples along the timeline (often \~100 points). Sample start offset. Sample width. Normalized 0.0–1.0. High-intensity spans. The single highest-intensity span, when labeled. Span start. Span end. Normalized 0.0–1.0. For example `Most replayed`. `full`. Full-mode responses keep every fast field. `heatmap.markers` is typically about 100 points; truncated: ```json { "video_id": "kCc8FmEb1nY", "title": "Let's build GPT: from scratch, in code, spelled out.", "author": "Andrej Karpathy", "channel_id": "UCXUPKJO5MZQN11PqgIvyuvQ", "channel": { "id": "UCXUPKJO5MZQN11PqgIvyuvQ", "title": "Andrej Karpathy", "url": "https://www.youtube.com/@AndrejKarpathy", "subscribers": "1.69M subscribers", "avatar_url": "https://yt3.ggpht.com/..." }, "length_seconds": 6980, "view_count": 7850809, "published": 1673913600, "like_count": 169183, "chapters": [ { "title": "intro: ChatGPT, Transformers, nanoGPT, Shakespeare", "start_time_seconds": 0, "time_description": "0:00" } ], "heatmap": { "markers": [ { "start_seconds": 0, "duration_seconds": 69.8, "intensity": 0.12 }, { "start_seconds": 1047, "duration_seconds": 69.8, "intensity": 1 } ], "peaks": [ { "start_seconds": 1047, "end_seconds": 1465.8, "intensity": 1, "label": "Most replayed" } ], "most_replayed": { "start_seconds": 1047, "end_seconds": 1465.8, "intensity": 1, "label": "Most replayed" } }, "mode": "full" } ``` ```ts const id = "kCc8FmEb1nY"; const params = new URLSearchParams({ mode: "fast" }); const response = await fetch( `https://api.ytapi.dev/v1/videos/${id}?${params}`, { headers: { Authorization: `Bearer ${process.env.YT_API_KEY}`, }, }, ); const video = await response.json(); console.log(video.title, video.view_count, video.mode); ``` ```python import os import requests video_id = "kCc8FmEb1nY" response = requests.get( f"https://api.ytapi.dev/v1/videos/{video_id}", headers={"Authorization": f"Bearer {os.environ['YT_API_KEY']}"}, params={"mode": "full"}, ) video = response.json() print(video["title"], video.get("like_count")) ``` ```bash curl -X GET "https://api.ytapi.dev/v1/videos/kCc8FmEb1nY?mode=fast" \ -H "Authorization: Bearer $YT_API_KEY" curl -X GET "https://api.ytapi.dev/v1/videos/kCc8FmEb1nY?mode=full" \ -H "Authorization: Bearer $YT_API_KEY" ``` ```go package main import ( "fmt" "net/http" "os" ) func main() { req, _ := http.NewRequest("GET", "https://api.ytapi.dev/v1/videos/kCc8FmEb1nY?mode=fast", nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("YT_API_KEY")) resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() fmt.Println(resp.Status) } ``` ```json { "video_id": "kCc8FmEb1nY", "title": "Let's build GPT: from scratch, in code, spelled out.", "author": "Andrej Karpathy", "channel_id": "UCXUPKJO5MZQN11PqgIvyuvQ", "channel": { "id": "UCXUPKJO5MZQN11PqgIvyuvQ", "title": "Andrej Karpathy", "url": "https://www.youtube.com/channel/UCXUPKJO5MZQN11PqgIvyuvQ" }, "description": "We build a Generatively Pretrained Transformer (GPT)...", "length_seconds": 6980, "view_count": 7850809, "keywords": ["gpt", "transformer", "nanogpt"], "thumbnails": [ { "url": "https://i.ytimg.com/vi/kCc8FmEb1nY/default.jpg", "width": 120, "height": 90 } ], "available_languages": [ { "code": "en", "name": "English (auto-generated)", "kind": "asr", "is_translatable": true } ], "mode": "fast" } ``` --- # Videos Video metadata without consuming YouTube Data API v3 quotas. Cached across L1/L2 memory and NVMe. `mode=fast` is 1 credit. `mode=full` is 2 credits and returns a different response body. Get video