Subtext API
Transcripts of YouTube videos for programs and AI agents: JSON, plain text, Markdown, SRT or WebVTT over a REST API, and the same two calls as a remote MCP server.
- Base URL:
https://youtubevideototranscript.com/api/v1 - Auth:
Authorization: Bearer <key>. Keys start withstx_and a new account gets 100 free credits. - One credit buys one transcript: every successful transcript request costs one. Errors cost nothing.
- Also for machines: this page as Markdown, OpenAPI 3.1 schema, try it in a browser (the page loads Swagger UI from cdn.jsdelivr.net), llms.txt.
- Clients must ignore response fields and error codes they do not know: both may be added.
Quick start
export SUBTEXT_API_KEY=stx_...
curl "https://youtubevideototranscript.com/api/v1/transcript?video=dQw4w9WgXcQ&format=text×tamps=true" \
-H "Authorization: Bearer $SUBTEXT_API_KEY"
Authentication
Send the key in the Authorization header of every request: Authorization: Bearer stx_.... A key belongs to one account; an account can hold several keys and they share its credits and its rate limit. A key in front-end code is public: call the API from a server. The dashboard's session cookie is not accepted.
GET /v1/transcript
Returns the transcript of one video. Costs 1 credit for every transcript returned: the same video again, another language or another format is another request and another credit. Requests sent at the same time for one new video share one fetch, and each of them is charged.
| Parameter | Required | Default | Values |
|---|---|---|---|
video | yes | 11-character video id or any YouTube URL (URL-encode a URL) | |
lang | no | the video's own language | caption language code, e.g. en, pt-BR |
translate | no | target language code. Best effort: YouTube often refuses; a failure costs nothing | |
format | no | json | json, text, md, srt, vtt |
timestamps | no | false | true starts each paragraph of format=text with [m:ss] |
curl -G "https://youtubevideototranscript.com/api/v1/transcript" \
--data-urlencode "video=https://www.youtube.com/watch?v=dQw4w9WgXcQ" \
-H "Authorization: Bearer $SUBTEXT_API_KEY"
format=json answers application/json (values below are an example):
{
"video_id": "dQw4w9WgXcQ",
"url": "https://youtu.be/dQw4w9WgXcQ",
"title": "Example video",
"author": "Example Channel",
"author_url": "https://www.youtube.com/@example",
"language": "en",
"language_name": "English (auto-generated)",
"source_language": "en",
"translated": false,
"auto_generated": true,
"duration": 4.5,
"segments": [
{
"start": 0.0,
"duration": 2.0,
"text": "Hello and welcome."
},
{
"start": 2.0,
"duration": 2.5,
"text": "Today: one idea, explained."
}
]
}
start and duration are seconds. Strings that are unknown are "", never null. The other formats answer the raw body: text is text/plain, md is text/markdown, srt is application/x-subrip, vtt is text/vtt.
GET /v1/languages
Lists the caption languages of a video. Free, but the key is required and the request counts against the rate limit. Use it to choose lang before asking for a transcript. A listing we already hold is answered without asking YouTube. For a video we have not seen yet it asks YouTube for the caption tracks only: no transcript is fetched or stored. That request can fail like a transcript request, with the same 503 and 504.
curl "https://youtubevideototranscript.com/api/v1/languages?video=dQw4w9WgXcQ" \
-H "Authorization: Bearer $SUBTEXT_API_KEY"
{
"video_id": "dQw4w9WgXcQ",
"title": "Example video",
"author": "Example Channel",
"author_url": "https://www.youtube.com/@example",
"default_language": "en",
"languages": [
{
"code": "en",
"name": "English (auto-generated)",
"auto_generated": true,
"translatable": true
},
{
"code": "de",
"name": "German",
"auto_generated": false,
"translatable": true
}
],
"translation_languages": [
{
"code": "fr",
"name": "French"
}
]
}
GET /v1/account
Free. Tells a program how many credits are left. Counts against the rate limit.
curl "https://youtubevideototranscript.com/api/v1/account" \
-H "Authorization: Bearer $SUBTEXT_API_KEY"
{
"credits_remaining": 99,
"plan": "free",
"rate_limit_per_minute": 120
}
Response headers
Every answer of the three endpoints carries these once the key is accepted, errors included:
| Header | Meaning |
|---|---|
X-Credits-Charged | 1 when this request spent a credit, otherwise 0 |
X-Credits-Remaining | credits left on the account |
X-RateLimit-Limit | requests per minute allowed for the account |
X-RateLimit-Remaining | requests left right now |
X-RateLimit-Reset | seconds until the limit is full again (a number of seconds, not a clock time) |
RateLimit-Policy, RateLimit | the same numbers in the IETF draft format |
X-Request-Id | quote it when you report a problem |
Retry-After | on 429 and 503: seconds to wait before retrying |
Until a key is accepted an answer carries X-Request-Id (and Retry-After on 429 and 503) and none of the credit and rate-limit headers. The same goes for 404 not_found and 405 method_not_allowed, even with a valid key: they do not count against the limit. Answers to CORS preflight requests carry none of these headers.
Answers of /api/v1/* are never cacheable (Cache-Control: no-store). Any origin may call the API from a browser (CORS is open, without credentials).
Errors
Every error is JSON in one shape. Branch on code; message is for people and may change.
{
"error": {
"code": "insufficient_credits",
"message": "You are out of credits.",
"status": 402,
"retryable": false,
"docs_url": "https://youtubevideototranscript.com/api#error-insufficient_credits",
"request_id": "req_97514c8eed0d4cc7a7b9dc9b",
"credits_remaining": 0,
"topup_url": "https://youtubevideototranscript.com/account/billing"
}
}
| Status | Code | Meaning and what to do | Retry |
|---|---|---|---|
| 401 | missing_api_key | No key in the request. Send it as a Bearer token in the Authorization header. | no |
| 401 | invalid_api_key | The key is mistyped or was revoked. Create a new one on your account page. | no |
| 402 | insufficient_credits | No credits left and this video is new to your account. The error carries credits_remaining and topup_url. | no |
| 404 | no_captions | The video has no captions. Do not retry. | no |
| 404 | language_not_available | No captions in that language. Retry with a code from available_languages in the error. | no |
| 404 | video_unavailable | The video does not exist, is private, was removed or cannot be played. | no |
| 404 | age_restricted | The video is age-restricted, so its captions cannot be read. | no |
| 404 | translation_not_available | YouTube cannot translate these captions into that language. | no |
| 404 | not_found | There is no such address under /api; an address with a trailing slash is one of them. Not counted against the rate limit. | no |
| 405 | method_not_allowed | The endpoints answer GET only, so HEAD is refused too. Not counted against the rate limit. | no |
| 422 | invalid_video | video is not an 11-character video id or a YouTube URL. | no |
| 422 | invalid_parameter | A parameter is missing or outside its documented values. The error names it in param. | no |
| 429 | rate_limited | One of three limits of the account: more requests than its limit per minute, more transcript requests running at once than it has credits, or more than 4 language listings waiting for YouTube at once. Without a valid key: too many requests from your network address. Wait Retry-After seconds, then retry. | yes |
| 500 | internal_error | Something went wrong on our side. Retry, and send us the request_id if it keeps happening. | yes |
| 503 | source_unavailable | YouTube is not answering right now. Wait Retry-After seconds, then retry. | yes |
| 503 | busy | Our queue of fetches is full, or our database was locked for a moment. Nothing was charged. Wait Retry-After seconds, then retry. | yes |
| 504 | timeout | The transcript took too long. It is still being fetched: retry in a few seconds. The request is charged when its transcript arrives, and the retry is then free. | yes |
Extra members: param (422), available_languages (language_not_available), credits_remaining and topup_url (402), retry_after (429 and 503, the same number as the Retry-After header).
Credits
- 1 credit = 1 transcript returned: every successful request to
/v1/transcript, and every successfulget_transcriptcall over MCP, costs one credit. The price is the same whether we fetch the transcript from YouTube or already hold it. - The same video again is another request and another credit; so is another language, translation or format. Keep a transcript you will need twice.
- An error costs nothing. One answer needs a second look: 504
timeoutmeans the transcript is still on its way (next but one line). - Requests sent at the same time for one new video share one fetch, and each of them is charged.
- A request that you abandon, or that answers 504
timeout, while we are still waiting for YouTube is charged all the same once its transcript arrives. You received nothing for that credit, so your next request for that video is free, once. That free request is answered from what we already hold: a transcript we still have to fetch, such as another language of the video, needs a credit. If that fetch fails, nothing is charged. Abandoning a request for a video we already hold costs nothing. /v1/languagesand/v1/accountare free.- A new account starts with 100 free credits (once per mailbox, and for the first 2 accounts created from one network address in 24 hours).
- Pricing
Limits
- 120 requests per minute per account, all its keys together, the REST API and the MCP server counted as one. Over the limit: 429
rate_limitedwithRetry-After. - Transcript requests an account has running at one time never outnumber its credits, and at most 4 of its language listings wait for YouTube at once. Past either: 429
rate_limitedwithRetry-After: 5. - A request waits up to 55 seconds for a video that is not cached yet, then answers 504
timeout. The fetch goes on: retry in a few seconds. The request is charged when its transcript arrives (see Credits), and the retry is then free. - REST API requests without a valid key are limited per network address.
MCP server
The same two calls for AI agents, as a remote MCP server (Streamable HTTP, POST only, plain JSON answers):
- URL:
https://youtubevideototranscript.com/mcp - Sign-in for apps (OAuth): give an app that supports it only the URL above. It opens your browser, you sign in and allow it, and its calls are charged to your account. You can disconnect an app on your account page.
- Or send the same key as the REST API:
Authorization: Bearer <key>. - For client authors: authorization code with PKCE (S256). An app is known by the metadata document it publishes (the address of the document is its client_id), or registers itself (RFC 7591). Refresh tokens are replaced on every use. There are no scopes. Start from the 401 of the server, or from
https://youtubevideototranscript.com/.well-known/oauth-protected-resource/mcp. An access token lives 1 hour and opens the MCP server only, not the REST API. - Credits work exactly as in the REST API, and an account's API and MCP calls share its credits and its rate limit.
- Every tool call that reaches the server spends one request of the limit, also a call with wrong arguments or an unknown tool name.
initializeandtools/listspend none. - The server opens no event streams and offers no subscriptions;
GETanswers 405. A request body over 64 KB answers 413. - A refusal reaches the model as the error text of the tool,
Error executing tool <name>: <reason>(out of credits, no captions, too many requests, the service is busy). An unexpected failure on our side readsError executing tool <name>with no reason.
Tools:
get_transcript(video, lang, translate, format, timestamps): Get the transcript (captions) of a YouTube video. Costs 1 credit for every transcript returned: asking again for the same video, in any language or format, costs another credit. A failed call costs nothing.list_languages(video): List the caption languages of a YouTube video and the languages its captions can be translated into, as JSON. Free. Use it to choose lang for get_transcript.
format is one of text (the default), json, md, srt, vtt; timestamps defaults to true.
Claude Code, one command:
claude mcp add --transport http subtext https://youtubevideototranscript.com/mcp \
--header "Authorization: Bearer $SUBTEXT_API_KEY"
Claude Code, .mcp.json in the project (the type line is required):
{
"mcpServers": {
"subtext": {
"type": "http",
"url": "https://youtubevideototranscript.com/mcp",
"headers": {
"Authorization": "Bearer ${SUBTEXT_API_KEY}"
}
}
}
}
Cursor, .cursor/mcp.json:
{
"mcpServers": {
"subtext": {
"url": "https://youtubevideototranscript.com/mcp",
"headers": {
"Authorization": "Bearer ${env:SUBTEXT_API_KEY}"
}
}
}
}
Versioning
The version is in the path (/api/v1). Within v1 changes are additive only: new optional parameters, new response fields, new error codes. Treat an unknown error code by its HTTP status.