API reference

A REST API for YouTube transcripts, plus the search and listing endpoints for finding the videos worth transcribing. One envelope for every response, one credit per call.

Base URLhttps://api.transcriptout.com

Data endpoints sit under /api, for example https://api.transcriptout.com/v1/transcript. The MCP endpoint (/mcp) is at the root.

Quick start

Create a key, send one request, read the envelope. Three minutes.

  1. 1
    Create an API key

    Open the API Keys page in your dashboard and create one. It's shown once, so store it somewhere safe.

  2. 2
    Call the API

    Ask for a transcript as plain text. Any YouTube URL works in place of the id.

    request.sh
    curl "https://api.transcriptout.com/v1/transcript?video=dQw4w9WgXcQ&format=text" \
      -H "Authorization: Bearer sk_your_key_here"
  3. 3
    Read the envelope

    Check ok, then read data. On failure, branch on code, never on the wording of detail.

    response.json
    {
      "ok": true,
      "request_id": "req_a1b2c3d4e5f6",
      "data": {
        "video_id": "dQw4w9WgXcQ",
        "language": "en",
        "kind": "manual",
        "transcript": "…the transcript, as one block of text…"
      }
    }

Authentication

One header on every request.

Pass your key as a Bearer token. A key is shown once. We keep only a hash, so a lost key is replaced, not recovered. Revoking is immediate.

request.sh
curl "https://api.transcriptout.com/v1/video?id=dQw4w9WgXcQ" \
  -H "Authorization: Bearer sk_your_key_here"

# missing or invalid key -> 401
# {"ok":false,"code":"unauthorized","detail":"Invalid api key","request_id":"req_…"}

Keep keys server-side. Anything shipped to a browser or a mobile app is readable by your users, so proxy the call through your own backend.

MCP

The same endpoints as tools for AI assistants.

Every data endpoint above is also an MCP tool, so Claude, Cursor and other MCP clients can call it mid-conversation. Same key, same credits, free plan included.

POST https://api.transcriptout.com/mcp

Streamable HTTP, stateless JSON-RPC: initialize , tools/list , tools/call , ping . No event stream and no session, so GET answers 405 by design. The key goes in every request. A bad key is a plain HTTP 401, not a tool error.

Connect it

Same URL everywhere, only the way of passing the key differs.

One command in the terminal, the key travels in a header.

terminal.sh
claude mcp add --transport http youtube-transcripts \
  https://api.transcriptout.com/mcp \
  --header "Authorization: Bearer sk_your_key_here"

Then ask the assistant something concrete, for example “get the transcript of this video and summarize it” with a link. It picks the right tool by the task.

Tools

Thin wrappers over the endpoints above: same parameters, same envelope inside the result, same errors.

get_transcriptTranscript of a video by id or URL. Plain text by default, timed segments with format=json. Set segment= to size those segments, 500–1500 for retrieval chunks. Add video_metadata=true for the title and channel in the same call.
get_video_infoTitle, channel, duration, views and available languages, without the subtitles, for when the transcript itself isn't wanted.
search_youtubeSearch YouTube for videos or channels, with pagination.
list_channel_videosVideos of a channel, newest first. Accepts @handle, channel id or URL.
search_channel_videosSearch inside one channel using YouTube's native relevance ranking.
latest_channel_videosThe most recent uploads of a channel, the fastest way to check what is new.
list_playlist_videosVideos of a playlist in playlist order, by id or URL.
search_playlist_videosFind videos in a playlist by a substring of the title.
submit_transcripts_jobQueue up to 4,000 videos at once and get a job id back immediately. Takes the same options as get_transcript, segment= included, one set for the whole job. 1 credit per video, charged on submit. Needs a user key.
get_transcripts_jobProgress of a batch job. Ready, failed and still pending. Free to poll.
get_transcripts_resultsFinished transcripts from a batch job, readable while the rest are still running. Free.
cancel_transcripts_jobStop a batch job. Only videos not started yet are refunded. Free.

Credits and limits

  • A tool call costs the same credits as the matching REST endpoint: 1 credit by default.
  • Connecting, listing tools and pings are free. So are the batch job tools: submitting charges 1 credit per video, and checking progress, reading results and canceling cost nothing.
  • Refund rules match REST: calls we reject before touching YouTube are credited back, and so is a 503. If we couldn't serve you, you don't pay for it.
  • The balance after each call comes back in the X-Credits-Remaining header.
  • Tool calls show up in Logs under their endpoint with method MCP.
  • Rate limits are shared with the REST API: one pool per key.

Protocol problems come back as a JSON-RPC error, a failed call as isError with our usual envelope inside. MCP is part of every plan, free included: if a key works for REST, it works here.

Troubleshooting

The Authorization header is missing or the key is wrong. Check that the value starts with “Bearer sk_” and that the key is still active. Revoking a key takes effect immediately.

The balance is empty. Tool errors reach the model in the same envelope as REST errors, with a machine-readable code. Top up to continue. The free grant is one-time and isn't renewed. Connecting and listing tools keeps working either way.

Most clients enable tools per chat or per agent. Check that the server is switched on in the client settings, then ask something concrete that mentions a video or a channel: the model picks tools by the task.

It can't send custom headers natively, so the config goes through the mcp-remote bridge (see the Claude Desktop tab above). Node.js has to be installed, npx downloads the bridge on first run.

Expected. The server is stateless Streamable HTTP: clients POST JSON-RPC messages to the endpoint, there's no event stream and no session to subscribe to.

Response envelope

Same shape for every endpoint, success or failure.

success.json
{
  "ok": true,
  "request_id": "req_a1b2c3d4e5f6",
  "data": { }
}
error.json
{
  "ok": false,
  "code": "not_found",
  "detail": "No subtitles for requested language",
  "request_id": "req_a1b2c3d4e5f6"
}
ok
Boolean success flag, the one check you always start with.
data
The payload, present only on success. Its shape is documented per endpoint below.
code
Machine-readable error slug, present only on failure. This is what your code should switch on.
detail
Human-readable message. Safe to log or surface, but the wording can change, so don't match on it.
request_id
Trace id for this call, on both success and failure. Also in the X-Request-ID header and in your Logs page.

Response headers

X-Request-IDThe same trace id as in the body. Present on every response.
X-Credits-RemainingYour balance after the call. Cheap way to monitor spend without polling anything.
X-CacheHIT or MISS on transcript responses, telling you whether we served it from cache or fetched it live.
Retry-AfterSeconds to wait before retrying. Sent with 429, 502 and 503.

Errors

Branch on code, never on the HTTP status or the detail text.

code is a stable machine-readable slug. detail is a human sentence we may reword at any time. The Refund column shows whether the call is credited back. See Credits .

400bad_requestInvalid input. A malformed video id, an unknown type value.Refunded
401unauthorizedMissing or invalid Bearer token. Revoked keys land here too.Refunded
402insufficient_creditsYour balance is empty. The call isn't performed at all.Not charged
403forbiddenThe key exists but isn't allowed to do this.Not charged
404not_foundThe video has no subtitles at all, or none in the language you asked for, or the video / channel / playlist doesn't exist. Terminal. Retrying won't change it.Charged
409subscription_existsThis account already has a live subscription with another payment provider. Cancel it before subscribing again.Not charged
409key_limit_reachedYou already hold the maximum number of API keys. Revoke an unused one first.Not charged
410goneThe video was removed or made private.Charged
413payload_too_largeThe request body exceeds the allowed size, or a batch job lists more videos than one job may hold.Refunded
422validation_errorA parameter failed validation. Wrong type, out of range.Refunded
429rate_limitedToo many requests. Back off and retry after Retry-After seconds.Refunded
451unavailable_for_legal_reasonsAge-restricted, members-only or login-required video.Charged
500internalSomething broke on our side. Send us the request_id.Charged
501not_implementedThe feature is switched off on this deployment. Payments with no provider configured, for instance.Not charged
502upstream_errorYouTube or our proxy layer failed. Safe to retry.Charged
503service_unavailableWe couldn't serve it right now: queue full, upstream throttling or a backend down. We already retried internally before answering, so wait out Retry-After instead of retrying immediately. Not to be confused with 404, which is terminal.Refunded

Every response carries a request_id , in the body and in the X-Request-ID header. Include it when you write to support. It's what lets us find the exact call in our logs.

Credits

One balance, one credit per call.

  • Every call costs 1 credit, whether or not the answer is the one you wanted. Cached and freshly fetched responses cost the same.
  • The balance left after a call comes back in the X-Credits-Remaining header.
  • Free credits are granted once at signup and aren't renewed. Plan credits are spent FIRST. They are the ones that expire at the monthly boundary, so anything else would burn credits that would have survived.
  • Refunded when the failure is on us: 400, 401, 413, 422, 429 and 503.
  • Duplicate calls still cost a credit each, even when we serve them from one fetch.
  • Running out returns 402 insufficient_credits without performing the call.
  • Every charge, refund and error is itemized on the Logs page with its request_id.

A 404 or 410 is charged. Both mean we asked YouTube and got a real answer. "This video has no transcript" is a result, not a failure. Requests we reject before touching YouTube come back free, and so does a 503: if we couldn't serve you, you don't pay for it.

Rate limits

200 requests per minute per key.

Exceeding the limit returns 429 rate_limited with a Retry-After telling you how many seconds are left. A throttled request is never charged, and the window slides, so bursting right after a rejection just gets rejected again.

How many requests to run in parallel. Take your rate times how long one answer takes: at 3 per second and roughly 3 seconds for a video we have not fetched before, that is about 10 in flight, and 8 is a safe default. Cached videos come back in milliseconds and cost you none of that budget, so a workload with repeats runs far ahead of this figure.

Opening many more connections than that doesn't go faster: your key holds a fixed share of our fetching capacity, and anything above it is answered 503 with Retry-After straight away rather than left to stall. Duplicates collapse, so the same video asked for twice at once is fetched once, and both 429 and 503 are refunded, so backing off costs you nothing.

For a long list, don't pace it yourself.POST /v1/transcripts takes up to 4,000 videos in one request and works through them at whatever rate is available, waiting for the window instead of refusing. Nothing is lost if your connection drops, progress is free to poll, and you collect the results when the job is done.

retry.sh
# 429 -> read Retry-After and wait it out. Throttled calls are never charged.
{"ok":false,"code":"rate_limited","detail":"Rate limit 200/min exceeded","request_id":"req_…"}
# Retry-After: 12

Pagination

Cursor tokens only, there's no offset.

List endpoints return next_page_token and has_more . Pass the token back for the next page while has_more is true. There's no offset , and page 400 costs the same as page 1.

paginate.sh
# page 1
curl "https://api.transcriptout.com/v1/channel/videos?name=@Fireship&limit=50" \
  -H "Authorization: Bearer sk_your_key_here"

# next page, the token replaces id and limit
curl -G "https://api.transcriptout.com/v1/channel/videos" \
  --data-urlencode "next_page_token=2b7e41d9c8305a6f" \
  -H "Authorization: Bearer sk_your_key_here"

The token replaces the original query parameters, with no need to repeat id or q . The one endpoint without tokens is /v1/playlist/search , which reports scanned and truncated instead.

Video field formats

Identical across every endpoint that returns videos.

durationstring"8:01" · "4:00:00""M:SS", or "H:MM:SS" once it passes an hour.
view_countstring | null"91K views" · "501,281 views"The string YouTube renders, passed through. Usually abbreviated (K/M/B), sometimes an exact count with separators, and exact below 1000. Handle both if you parse it into a number.
publishedstring | null"5 days ago" · "2 years ago"Relative date, as displayed by YouTube. The one exception is /v1/channel/latest, which returns an ISO timestamp.
thumbnailsarray[{ url, width, height }]Five sizes from 120×90 to 1280×720. The two largest can 404 for some videos, so handle onerror.

These are display strings, not numbers. They mirror what YouTube shows. If you need to sort or compute with durations, parse them on your side.

Endpoints

12 endpoints, one envelope. Parameters, an example call and the response for each.

GET/v1/transcript1 credit

Get a transcript

The main endpoint: subtitles of a single video, by id or by any YouTube URL. Returns timed segments by default, or one flat string with format=text.

Parameters

video * type stringdefault Video id (11 chars) or any YouTube URL (watch, youtu.be, shorts, embed).
langtype stringdefault enLanguage code of the track: en, ru, es-419. No track in that language → 404. A video with no subtitles at all also answers 404, not 503.
kindtype manual | autodefault manual if it existsWhich track to take. Omit it to prefer the human-made one and fall back to auto-generated.
formattype json | text | srt | vtt | srv3default jsonChanges only the type of data.transcript. See the note below.
segmenttype integerdefault 180 (80 for srt/vtt), auto tracks onlyMax characters per segment, 20–5000. Sentences are packed up to it and never cut mid-thought: 40–80 for subtitle lines, 500–1500 for embedding chunks. Pass it and it applies to manual tracks too, so one size means one granularity whichever track answers. Not valid with format=srv3.
video_metadatatype booleandefault falseAlso include data.metadata with the same fields GET /v1/video returns.
downloadtype booleandefault falseReturn the file itself instead of the envelope (srt, vtt, srv3 or text), with a Content-Disposition so curl -o writes it straight to disk. Refused with format=json and with video_metadata. Errors still come back as the usual JSON envelope.

* required

Example

request.sh
curl "https://api.transcriptout.com/v1/transcript?video=dQw4w9WgXcQ&lang=en" \
  -H "Authorization: Bearer sk_your_key_here"
response.json
{
  "ok": true,
  "request_id": "req_a1b2c3d4e5f6",
  "data": {
    "video_id": "dQw4w9WgXcQ",
    "language": "en",
    "kind": "manual",
    "transcript": [
      { "text": "Look at the shape of this curve", "start": 18.64, "duration": 3.2 },
      { "text": "it tells you the reaction is second order", "start": 21.84, "duration": 3.12 }
    ],
    "available_langs": [
      { "code": "en", "kind": "manual", "name": "English" },
      { "code": "en", "kind": "auto", "name": "English (auto-generated)" }
    ]
  }
}

Response fields

Inside data

video_id
string
The id we resolved from your input.
language
string
Language code of the track actually returned.
kind
manual | auto
Which track served the transcript.
transcript
array | string
Segments for json, a string for text, srt, vtt and srv3.
available_langs
array
Every track the video has: { code, kind, name }.
metadata
object
Only with video_metadata=true.

Every format comes back in the same JSON envelope. Only data.transcript changes type. json gives an array of segments with start and duration in seconds, text gives one string with lines joined by \n, srt and vtt give a ready subtitle file body, cut to 80-character cues on an auto track unless segment= says otherwise. srv3 gives YouTube's raw timedtext XML as a string (404 if that track has no srv3 source).

By default segmentation follows the track: a manual one keeps the author's own line breaks (~40 characters), while an auto-generated one is rebuilt into sentences (~95). Pass segment= to decide the size yourself and get the same granularity from either.

Auto-generated tracks in many languages arrive with no punctuation at all. There we cut on the speaker's own pauses rather than on a character count, so a segment ends where the sentence did.

Responses carry X-Cache: HIT when the transcript came from our cache and MISS when we fetched it just now. A MISS takes a few seconds. A HIT is immediate. The price is the same either way.

Inside a transcript segment, duration is a number of seconds (4.12), not the "M:SS" string used in video listings.

POST/v1/transcripts1 credit per video

Get transcripts in bulk

Hand over up to 4,000 videos at once and collect the transcripts as they land. It's the single-video endpoint in a loop (same price, same cache, same options) with the loop written on our side. You're never left holding the request: the answer is a job id, and the results can be read as they arrive.

How it works

  1. 1POST/v1/transcripts1 credit per videoHand over the list. Answers 202 with a job_id, not the transcripts.
  2. 2GET/v1/transcripts/{job_id}freeCheck on it whenever you like: done, ready, failed, pending.
  3. 3GET/v1/transcripts/{job_id}/resultsfreeRead the transcripts, page by page, already possible while it runs.
  4. 4GET/v1/transcripts/{job_id}/results/{video_id}freeOne video out of the job, by id. Add download=true to save it as a file.
  5. 5POST/v1/transcripts/{job_id}/cancelfreeChanged your mind: stops it and refunds whatever it has not reached.

Parameters

videos * type string[]default Video ids or YouTube URLs, up to 4,000 per job. Duplicates are collapsed before you're charged.
langtype stringdefault enOne language for the whole job.
formattype json | text | srt | vtt | srv3default jsonExactly as on /v1/transcript. It changes only the type of transcript in each item.
kindtype manual | autodefault manual if it existsWhich track to take, for every video in the job.
segmenttype integerdefault 180 (80 for srt/vtt), auto tracks onlyMax characters per segment, 20–5000, exactly as on /v1/transcript, one size for the whole job. Not valid with format=srv3.
video_metadatatype booleandefault falseAdd the video metadata to each item, the same fields GET /v1/video returns.
Idempotency-Keytype headerdefault Optional. A retry with the same key and the same list returns the SAME job instead of opening a second one and charging twice. Kept for 24 hours.

* required

Example

request.sh
curl -X POST "https://api.transcriptout.com/v1/transcripts" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer sk_your_key_here" \
  -d '{"videos": ["dQw4w9WgXcQ", "kX7Zj9NqGkY"], "format": "text"}'
response.json
{
  "ok": true,
  "request_id": "req_a1b2c3d4e5f6",
  "data": {
    "job_id": "17ef4c30218c47bc98bb5ac072d564e4",
    "status": "queued",
    "count": 2,
    "lang": "en",
    "format": "text",
    "credits": { "charged": 2, "refunded": 0 }
  }
}

Response fields

Inside data

job_id
string
Use it to poll the job and to read its results.
status
string
queued → running → done, or cancelled.
count
integer
How many videos the job holds, after duplicates.
lang
string
The language it will fetch, echoed back.
format
string
The format the results will come in, echoed back.
credits
object
{ charged, refunded }. The whole list is charged up front.

Nothing waits on the request. The transcripts are read from …/results, and that works while the job is still running. The first page is there long before the last video is.

The pace is your rate limit: a job spends the same requests-per-minute your direct calls do, so a thousand videos take about what a thousand single calls would. Handing the list in costs one unit of that allowance, not one per video.

Every video reports its own status, and the ones that failed through no fault of yours are refunded automatically. One bad video never sinks the rest.

A user key (sk_…) is required, and up to 3 jobs can be unfinished at a time. A finished job and its results stay readable for 24 hours.

POST /v1/transcripts/{job_id}/cancel refunds only what the job has not reached yet. Videos already fetched cost real work and are in your results, so they stay charged.

GET/v1/transcripts/{job_id}free

Check a batch job

How far along a job is. Free and safe to call as often as you like. Polling work you have already paid for should not cost anything.

Parameters

job_id * type stringdefault The id POST /v1/transcripts gave you.

* required

Example

request.sh
curl "https://api.transcriptout.com/v1/transcripts/17ef4c30218c47bc98bb5ac072d564e4" \
  -H "Authorization: Bearer sk_your_key_here"
response.json
{
  "ok": true,
  "request_id": "req_a1b2c3d4e5f6",
  "data": {
    "job_id": "17ef4c30218c47bc98bb5ac072d564e4",
    "status": "running",
    "lang": "en",
    "format": "json",
    "count": 1000,
    "done": 240,
    "ready": 238,
    "failed": 2,
    "pending": 760,
    "credits": { "charged": 1000, "refunded": 2 },
    "created_at": "2026-08-08T11:37:59.680283+00:00",
    "finished_at": null
  }
}

Response fields

Inside data

job_id
string
The job this is about, the same id you asked for.
status
string
queued → running → done, or cancelled.
lang
string
The language the job was submitted with.
format
string
The format the job was submitted with.
count
integer
Videos in the job.
done
integer
How many have an answer, ready or failed.
ready
integer
How many came back with a transcript.
failed
integer
How many ended in an error.
pending
integer
Still to go.
credits
object
{ charged, refunded } as it stands right now.
created_at
string
When the job was accepted, ISO-8601.
finished_at
string | null
Null until the job ends.

Every 10–30 seconds is plenty. The job moves at your rate limit, so a thousand videos take about five minutes whatever you do.

You don't have to wait for done to start reading. …/results already has everything finished so far.

GET/v1/transcripts/{job_id}/resultsfree

Read batch results

The transcripts, in the order they landed, a page at a time. Works while the job is still running, so the first page is readable long before the last video is fetched.

Parameters

job_id * type stringdefault The id POST /v1/transcripts gave you.
limittype integerdefault 100Results per page, 1–500.
next_page_tokentype stringdefault The token from the previous page. Omit it for the first one.

* required

Example

request.sh
curl "https://api.transcriptout.com/v1/transcripts/17ef4c30218c47bc98bb5ac072d564e4/results?limit=2" \
  -H "Authorization: Bearer sk_your_key_here"
response.json
{
  "ok": true,
  "request_id": "req_a1b2c3d4e5f6",
  "data": {
    "job_id": "17ef4c30218c47bc98bb5ac072d564e4",
    "status": "running",
    "lang": "en",
    "format": "json",
    "count": 2,
    "has_more": true,
    "next_page_token": "3f0c7a91d4b28e65",
    "results": [
      {
        "status": "ready",
        "cache": "HIT",
        "video_id": "dQw4w9WgXcQ",
        "language": "en",
        "kind": "manual",
        "transcript": [
          { "text": "Look at the shape of this curve", "start": 18.64, "duration": 3.2 }
        ],
        "available_langs": [
          { "code": "en", "kind": "manual", "name": "English" }
        ]
      },
      {
        "status": "error",
        "video_id": "kX7Zj9NqGkY",
        "code": "not_found",
        "detail": "No subtitles for this video"
      }
    ]
  }
}

Response fields

Inside data

job_id
string
The job this page belongs to.
status
string
The job status, so one call answers "is there more" and "is it over".
lang
string
The language the job was submitted with.
results
array
The page, in the order the results landed. A ready entry is exactly what GET /v1/transcript returns in data (video_id, language, kind, transcript) plus status and cache, so one parser reads both. A failed one is status: "error" with a machine code. Every entry names its own video_id.
count
integer
How many results are on this page.
has_more
boolean
Whether another page exists right now.
next_page_token
string | null
Pass it back to get the next page.
format
string
The format the job was submitted with.

has_more answers "is there another page right now", not "is the job finished". On a running job it goes false and then true again as more results land. The status field is what tells you the job is over.

Branch on the machine code, never on the text. The codes are the ones the single endpoint returns, so one error handler covers both.

POST/v1/transcripts/{job_id}/cancelfree

Cancel a batch job

Stops a job and refunds the videos it never started. Anything already fetched stays charged. It cost real work and it stays in your results, which canceling doesn't touch. Answers with the job status, in the same shape GET /v1/transcripts/{job_id} returns.

Parameters

job_id * type stringdefault The id POST /v1/transcripts gave you.

* required

Example

request.sh
curl -X POST "https://api.transcriptout.com/v1/transcripts/17ef4c30218c47bc98bb5ac072d564e4/cancel" \
  -H "Authorization: Bearer sk_your_key_here"
response.json
{
  "ok": true,
  "request_id": "req_a1b2c3d4e5f6",
  "data": {
    "job_id": "17ef4c30218c47bc98bb5ac072d564e4",
    "status": "cancelled",
    "lang": "en",
    "format": "json",
    "count": 1000,
    "done": 240,
    "ready": 238,
    "failed": 2,
    "pending": 760,
    "credits": { "charged": 1000, "refunded": 760 },
    "created_at": "2026-08-08T13:58:23.117847+00:00",
    "finished_at": "2026-08-08T14:04:11.402913+00:00"
  }
}

Response fields

Inside data

job_id
string
The job that was stopped.
status
string
cancelled, once it has been.
lang
string
The language the job was submitted with.
format
string
The format the job was submitted with.
count
integer
Videos the job held.
done
integer
What it managed to fetch. Those results remain readable.
ready
integer
How many of those came back with a transcript.
failed
integer
How many ended in an error before the stop.
pending
integer
What it never reached. This is what was refunded.
credits
object
{ charged, refunded }. Here refunded includes every video the job never started.
created_at
string
When the job was accepted, ISO-8601.
finished_at
string
When it was stopped.

The line is whether a video produced a result, not whether it was still queued, so the batch a worker is holding when you cancel is delivered and stays charged.

Your results aren't deleted. Everything fetched before the cancel stays readable from …/results for the usual 24 hours.

Canceling twice is safe: the second call changes nothing and refunds nothing again.

GET/v1/video1 credit

Get video info

Metadata for one video plus the list of transcript languages it has, without downloading any subtitles. Use it to check what exists before asking for a transcript.

Parameters

id * type stringdefault Video id or any YouTube URL.

* required

Example

request.sh
curl "https://api.transcriptout.com/v1/video?id=dQw4w9WgXcQ" \
  -H "Authorization: Bearer sk_your_key_here"
response.json
{
  "ok": true,
  "request_id": "req_a1b2c3d4e5f6",
  "data": {
    "video_id": "dQw4w9WgXcQ",
    "title": "Rick Astley - Never Gonna Give You Up",
    "channel": "Rick Astley",
    "channel_id": "UCuAXFkgsw1L7xaCfnd5JJOw",
    "duration": "3:33",
    "view_count": "1.6B views",
    "keywords": ["rick astley", "never gonna give you up"],
    "thumbnails": [
      { "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg", "width": 480, "height": 360 }
    ],
    "is_live": false,
    "available_langs": [
      { "code": "en", "kind": "manual", "name": "English" }
    ]
  }
}

Response fields

Inside data

video_id
string
Resolved video id.
title
string
Video title.
channel
string
Channel name.
channel_id
string
Channel id (UC…).
duration
string
"M:SS" or "H:MM:SS".
view_count
string | null
View count as YouTube renders it: usually abbreviated, sometimes exact.
keywords
string[]
Tags the author set, may be empty.
thumbnails
array
Five sizes: { url, width, height }.
is_live
boolean
Whether this is (or was) a live broadcast.
available_langs
array
Every transcript track: { code, kind, name }.

available_langs here is authoritative: if a language is listed, GET /v1/transcript will return it. This is the reliable way to check for subtitles. The has_captions flag in search results is only a hint.

Fresh data is cached for an hour, since titles and view counts move.

GET/v1/channel/videos1 credit

List channel videos

Everything on a channel's Videos tab, newest first, paginated. Accepts a handle, a channel id or a URL.

Parameters

name * type stringdefault @handle, channel name, UC… channel id, or channel URL. Optional if you pass next_page_token.
limittype integerdefault 100 · 500 with ids_onlyPage size, up to 100, or up to 500 in ids_only mode.
next_page_tokentype stringdefault Token from a previous response.
ids_onlytype booleandefault falseReturn video_ids[] instead of full objects, up to 500 per page.

* required

Example

request.sh
curl "https://api.transcriptout.com/v1/channel/videos?name=@Fireship&limit=50" \
  -H "Authorization: Bearer sk_your_key_here"

# just the ids, up to 500 per page, cheapest way to enumerate a channel
curl "https://api.transcriptout.com/v1/channel/videos?name=@Fireship&ids_only=true" \
  -H "Authorization: Bearer sk_your_key_here"
response.json
{
  "ok": true,
  "request_id": "req_a1b2c3d4e5f6",
  "data": {
    "channel": "@Fireship",
    "title": "Fireship",
    "count": 50,
    "videos": [
      {
        "video_id": "MWRPYBoCEaY",
        "title": "Rust in 100 Seconds",
        "channel": "Fireship",
        "duration": "2:29",
        "view_count": "1.9M views",
        "published": "2 years ago",
        "url": "https://www.youtube.com/watch?v=MWRPYBoCEaY",
        "thumbnails": [
          { "url": "https://i.ytimg.com/vi/MWRPYBoCEaY/hqdefault.jpg", "width": 480, "height": 360 }
        ]
      }
    ],
    "next_page_token": "2b7e41d9c8305a6f",
    "has_more": true
  }
}

Response fields

Inside data

channel
string
The identifier you passed in.
title
string
Channel name.
count
integer
Items on this page.
videos
array
video_id, title, channel, duration, view_count, published, url, thumbnails. Replaced by video_ids with ids_only.
next_page_token
string | null
Token for the next page.
has_more
boolean
Whether another page exists.

A pattern worth knowing: enumerate a channel with ids_only=true (500 ids per call), then fetch transcripts only for the ids you actually need. That keeps the number of billed calls proportional to the work you want, not to the size of the channel.

GET/v1/channel/latest1 credit

A channel's latest videos

The ~15 most recent uploads of a channel, straight from its RSS feed. The fastest and lightest way to poll a channel for new videos.

Parameters

name * type stringdefault @handle, channel name, UC… channel id, or channel URL.

* required

Example

request.sh
curl "https://api.transcriptout.com/v1/channel/latest?name=@Fireship" \
  -H "Authorization: Bearer sk_your_key_here"
response.json
{
  "ok": true,
  "request_id": "req_a1b2c3d4e5f6",
  "data": {
    "channel": "@Fireship",
    "channel_id": "UCsBjURrPoezykLs9EqgamOA",
    "count": 15,
    "videos": [
      {
        "video_id": "MWRPYBoCEaY",
        "title": "Rust in 100 Seconds",
        "published": "2026-07-29T14:02:11+00:00",
        "thumbnails": [
          { "url": "https://i.ytimg.com/vi/MWRPYBoCEaY/hqdefault.jpg", "width": 480, "height": 360 }
        ]
      }
    ],
    "video_ids": ["MWRPYBoCEaY"]
  }
}

Response fields

Inside data

channel
string
The identifier you passed in.
channel_id
string
Resolved UC… id.
count
integer
Number of entries, normally 15.
videos
array
video_id, title, published (ISO 8601), thumbnails.
video_ids
string[]
The same ids as a flat array, for convenience.

Use this instead of /v1/channel/videos when you only care about what is new. It's the cheapest endpoint we have and doesn't go through our proxy layer at all.

This endpoint is the exception to the field formats: published is a full ISO 8601 timestamp rather than "5 days ago", and there's no duration or view_count. The RSS feed doesn't carry them.

GET/v1/playlist/videos1 credit

List playlist videos

Videos of a playlist in playlist order, paginated. Accepts a playlist id or any URL containing list=.

Parameters

id * type stringdefault PL… playlist id, or a URL with a list= parameter. Optional if you pass next_page_token.
limittype integerdefault 100 · 500 with ids_onlyPage size, up to 100, or up to 500 in ids_only mode.
next_page_tokentype stringdefault Token from a previous response.
ids_onlytype booleandefault falseReturn video_ids[] instead of full objects, up to 500 per page.

* required

Example

request.sh
curl "https://api.transcriptout.com/v1/playlist/videos?id=PLillGF-RfqbbnEGy3ROiLWk7JMCuSyQtX" \
  -H "Authorization: Bearer sk_your_key_here"
response.json
{
  "ok": true,
  "request_id": "req_a1b2c3d4e5f6",
  "data": {
    "playlist": "PLillGF-RfqbbnEGy3ROiLWk7JMCuSyQtX",
    "title": "JavaScript Basics",
    "count": 100,
    "videos": [
      {
        "video_id": "hdI2bqOjy3c",
        "title": "JavaScript Crash Course",
        "channel": "Traversy Media",
        "duration": "1:40:29",
        "view_count": "4.2M views",
        "published": "6 years ago",
        "url": "https://www.youtube.com/watch?v=hdI2bqOjy3c",
        "thumbnails": [
          { "url": "https://i.ytimg.com/vi/hdI2bqOjy3c/hqdefault.jpg", "width": 480, "height": 360 }
        ]
      }
    ],
    "next_page_token": "81de37b4a95c6e02",
    "has_more": true
  }
}

Response fields

Inside data

playlist
string
The identifier you passed in.
title
string
Playlist title.
count
integer
Items on this page.
videos
array
Same video shape as /v1/channel/videos. Replaced by video_ids with ids_only.
next_page_token
string | null
Token for the next page.
has_more
boolean
Whether another page exists.

Something missing or wrong here? Write to support@transcriptout.com , we keep these docs in sync with the API.