Four top-level fields
{
"ok": true,
"request_id": "req_8f2c…",
"data": {
"kind": "transcript",
"video_id": "dQw4w9WgXcQ",
"platform": "youtube",
"language": "en",
"source": "captions",
"segments": [ … ]
},
"usage": { "credits_spent": 1, "balance": 99, "bytes": 14233 }
}- true on a 200 and a 202, false on every error.
okboolean - Unique per call, and also sent as the X-Request-Id header. Include it when you contact support.
request_idstring - The result. data.kind names its shape; the kinds are listed below.
dataobject - Credits charged for this call, your remaining balance and the bytes it used. An error carries none because failures are free, and neither does a 202: a job is charged on delivery.
usageobject
Segments or one string
Set timestamps in the request to choose how a single transcript comes back. The other key is absent, not null.
{
"source": "captions",
"segments": [
{ "start": 0, "duration": 3.5,
"text": "Welcome back to the channel." },
{ "start": 3.5, "duration": 4.1,
"text": "Today we're building a transcript search tool." },
{ "start": 7.6, "duration": 4.4,
"text": "First, we fetch the captions for every video." },
…
]
}- 0:00.0Welcome back to the channel.
- 0:03.5Today we're building a transcript search tool.
- 0:07.6First, we fetch the captions for every video.
- 0:12.0Then we index the text by timestamp.
Where the text came from
data.source says which path produced the transcript. The response shape is the same either way.
"captions""audio"mode: "audio", the audio is transcribed. Charged on delivery: 1 credit per started 5 minutes of audio. Send mode: "captions" to never pay for it. How AI Fallback Transcription works →When you get a 202
AI Fallback Transcription that can't finish inside the inline wait of about 45 seconds answers 202 with a job instead of a transcript. Poll poll_url, or pass a callback_url and the result is POSTed to you. The finished job carries the same transcript data as a 200 and is charged only on delivery.
{
"ok": true,
"request_id": "req_…",
"status": "processing",
"job_id": "asr_m3k1x9qz4vb2p7",
"poll_url": "/api/v2/transcripts/jobs/asr_m3k1x9qz4vb2p7",
"data": {
"kind": "transcript_job",
"video_id": "dQw4w9WgXcQ",
"platform": "youtube"
}
}Same envelope, ok is false
An error replaces data with error and carries no usage, because failures are free. Branch on error.code, not the message. When retry_with is present, it names what to change and send again.
{
"ok": false,
"request_id": "req_…",
"error": {
"code": "no_captions",
"number": 4103,
"message": "No caption track (manual or auto-generated) is available. Send mode \"auto\" to transcribe the audio instead.",
"docs": "https://transcriptfetch.com/docs/errors/no_captions",
"retry_with": {
"mode": "auto"
}
}
}data.kind
Branch on data.kind rather than on the endpoint you called: a batch entry and a single fetch carry the same transcript shape.
| kind | What data holds |
|---|---|
| transcript | A single transcript: video metadata plus segments or text. |
| video_list | A page of videos from a channel, profile, playlist or search, plus next_cursor. |
| playlist_list | A page of playlists: a YouTube channel's playlists or podcasts tab, or a playlist search, plus next_cursor. |
| channel_list | A page of channels from a YouTube channel search, plus next_cursor. |
| transcript_batch | One entry per requested video in data.results, each with an outcome. |
| transcript_job | The 202 envelope and job polling for AI Fallback Transcription. |
| monitor | A monitor. The other monitor endpoints answer monitor_list (your monitors and limits), monitor_event_list (a page of events), monitor_check (a check's result) and monitor_deleted. |
Single transcript
The /video endpoint returns kind: "transcript" with either segments or text, never both. The other key is absent, not null, so check for presence, not truthiness.
| timestamps | Returns | Shape |
|---|---|---|
true (default) | segments | An array; each segment's start and duration are in seconds, enough to render timestamps or export SRT/VTT |
false | text | One joined string |
Every transcript carries the same metadata, whichever platform or path served it. Every key is always present; null means unknown, never omitted. A cache hit, an inline audio result, a job poll and a webhook delivery all carry exactly these keys.
| Field | What it holds |
|---|---|
video_id | The item's id on its platform: the 11-character YouTube id, TikTok's numeric id, the Instagram shortcode, or the URL for a direct file |
url | The canonical link, accepted as-is by every transcript endpoint |
platform | youtube, tiktok, instagram or file |
title | The title; the post caption on TikTok and Instagram |
channel | The creator, as the platform names them: a channel name, a TikTok @handle, an Instagram username |
duration | Length in seconds |
language | The caption track's code, or the language detected during AI Fallback Transcription |
thumbnail_url | The poster image. TikTok and Instagram URLs are signed and expire, so copy the image rather than hotlinking it |
source | "captions" for an existing caption track, "audio" for AI Fallback Transcription (on by default when there are no captions; billed by length, see usage.credits_spent) |
{ "ok": true, "request_id": "req_…", "data": { "kind": "transcript", "video_id": "dQw4w9WgXcQ", "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "platform": "youtube", "title": "Example video", "channel": "Example Channel", "duration": 212, "language": "en", "thumbnail_url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/mqdefault.jpg", "source": "captions", "segments": [ { "start": 0, "duration": 3.5, "text": "We're no strangers to love" } ] }, "usage": { "credits_spent": 1, "balance": 99, "bytes": 14233 } }
Video list (channel / playlist / search)
/channel, /playlist and /search return kind: "video_list": a videos array of metadata and a next_cursor for the next page. Fetch the transcripts separately with the batch endpoint.
| Status | When |
|---|---|
404 | The channel, profile or playlist does not exist or is not public; error.code is "not_found" |
200 | It exists but has nothing to list: an empty videos array, free of charge |
503 | An upstream fault while listing, never an empty page |
A list can also carry a notice saying why a page is empty or may be incomplete, such as "This channel has no Live tab."
{ "ok": true, "request_id": "req_…", "data": { "kind": "video_list", "source": "search", "platform": "tiktok", "videos": [ { "videoId": "7398765432101234567", "url": "https://www.tiktok.com/@examplecreator/video/7398765432101234567", "title": "the post caption", "duration": 58, "channel": "@examplecreator", "publishedAt": "2026-08-30T14:02:00Z", "stats": { "plays": 1240000 } } ], "next_cursor": "eyJvIjoxMH0" }, "usage": { "credits_spent": 1, "balance": 98, "bytes": 0 } }
Playlist and channel lists
Some YouTube listings return playlists or channels instead of videos. Both page with next_cursor like a video list, and every row's url is accepted as-is by the playlist or channel endpoint.
kind | When | Each row carries |
|---|---|---|
playlist_list | A channel's playlists or podcasts tab, or a search with type: "playlist" | In playlists: playlistId, url, title, channel, videoCount |
channel_list | A search with type: "channel" | In channels: channelId, url, handle, title, subscribers (YouTube's rounded label), videoCount (always null: YouTube's channel results no longer show one), description |
The channel and search references show a full example of each.
Batch
The batch endpoint returns kind: "transcript_batch", one entry per requested video in data.results. Entries carry both text and segments (batch does not accept timestamps), plus an outcome:
outcome | The entry |
|---|---|
ok | The transcript |
processing | No caption track, so it went to AI Fallback Transcription (the default): a job_id and poll_url, nothing charged on the batch call, billed on delivery at the audio rate. Re-send the same batch once it finishes and the text comes back normally; polling is optional |
error | The same error block as a request-level failure, and no text fields |
Send mode: "captions" to have captionless entries fail as error with code no_captions (and a retry_with naming the audio mode) instead.
{ "ok": true, "request_id": "req_…", "data": { "kind": "transcript_batch", "results": [ { "video_id": "dQw4w9WgXcQ", "outcome": "ok", "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "title": "Example video", "channel": "Example Channel", "duration": 212, "language": "en", "thumbnail_url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/mqdefault.jpg", "source": "captions", "text": "Full transcript text …", "segments": [ { "start": 0, "duration": 3.5, "text": "Full transcript …" } ], "bytes": 14233 }, { "video_id": "9bZkp7q19f0", "outcome": "processing", "job_id": "asr_…", "poll_url": "/api/v2/transcripts/jobs/asr_…" }, { "video_id": "jNQXAC9IVRw", "outcome": "error", "error": { "code": "no_captions", "number": 4103, "message": "No caption track (manual or auto-generated) is available.", "docs": "https://transcriptfetch.com/docs/errors/no_captions", "retry_with": { "mode": "auto" } } } ] }, "usage": { "credits_spent": 1, "balance": 97 } }
Markdown and chunks for AI (beta)
Two optional request fields on /video and /batch repackage the transcript for an AI pipeline. Both are free, both work on every platform, and both are absent unless you ask, so a request without them answers exactly as before.
"markdown": trueaddsmarkdown: a title line, a line with the channel, length and link, then paragraphs that each open with their time. On YouTube each time links to that moment in the video. Use it when a model will quote or cite the transcript."chunks": trueaddschunks: the transcript split into pieces sized for an embedding model or vector store. Each piece is{ index, text, start, end, url, tokens }, whereurlopens the video atstart(on YouTube; on TikTok and Instagram it is the video's own link). Pieces break between caption lines, never mid-word, and each repeats the end of the previous one so a sentence on the boundary is never lost.startandendarenullwhen a transcript has no timings.- Size the pieces with
"chunks": { "max_tokens": 500, "overlap_tokens": 50 }(those are the defaults):max_tokensfrom 100 to 2000,overlap_tokensfrom 0 to 200 and smaller thanmax_tokens. Tokens are counted with OpenAI's o200k_base encoding, close to what Claude and Gemini count.
curl -X POST https://transcriptfetch.com/api/v2/transcripts/video \ -H "Authorization: Bearer $TRANSCRIPTFETCH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"video": "aircAruvnKk", "timestamps": false, "chunks": {"max_tokens": 500, "overlap_tokens": 50}}'
"chunks": [ { "index": 0, "text": "This is a 3. It's sloppily written and rendered at an extremely low resolution of 28x28 pixels, …", "start": 4.22, "end": 139.18, "url": "https://www.youtube.com/watch?v=aircAruvnKk&t=4s", "tokens": 494 }, { "index": 1, "text": "There are many many variants of neural networks, …", "start": 125.04, "end": 272.34, "url": "https://www.youtube.com/watch?v=aircAruvnKk&t=125s", "tokens": 494 } ]
That 18-minute video makes 9 pieces with the defaults. On batch, every ok entry gains the same keys. A captionless video that goes to AI Fallback Transcription remembers the options: its job poll and its webhook answer with markdown and chunks too. v1 does not take these fields.
Search with transcripts (beta)
Add transcripts to a YouTube, TikTok or Instagram video search and every video row comes back with a transcript, so one call goes from a query to what the videos say:
curl -X POST https://transcriptfetch.com/api/v2/transcripts/search \ -H "Authorization: Bearer $TRANSCRIPTFETCH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"query": "how neural networks learn", "captions": true, "limit": 10, "transcripts": {"timestamps": false, "chunks": true}}'
Each row's transcript has the same shape as a batch entry without video_id: an outcome (ok, processing or error), then text, segments, markdown and chunks as asked, or the error block. transcripts: true fetches caption transcripts with timestamps; the object takes mode, timestamps, markdown and chunks. The mode defaults to "captions" here, so a search never starts paid AI Fallback Transcription unless you send "mode": "auto" (captionless rows then come back processing with a job, as in a batch). limit can be at most 10 with transcripts, and type must be a video search.
It costs the page's credit plus 1 credit per caption transcript delivered; failed rows are free. An account that cannot cover the page and every row is refused with 402 before anything is fetched.
Field reference
data.kind:transcript,video_list,playlist_list,channel_list,transcript_batch,transcript_job, orme; the monitor endpoints answer withmonitor,monitor_list,monitor_event_list,monitor_checkandmonitor_deleted.text/segments: exactly one on single-transcript responses (chosen bytimestamps); both on batch entries and completed job results. Segments are{ start, duration, text }cues in seconds (speakeradded when diarized).next_cursor: pass back ascursorfor the next page of a list,nullwhen exhausted.outcome(batch results):ok,processing, orerror; failed entries carryerror(the standard block), andprocessingentries (captionless videos in AI Fallback Transcription) carryjob_idandpoll_url.video_id,url,platform,title,channel,duration,language,thumbnail_url(transcripts): the metadata block above, on every transcript-bearing response including job polls, webhooks and batch entries. Always present;nullwhen unknown.source(transcripts):"captions"or"audio".markdown,chunks(transcripts, beta): present only when the request asked for them; see above.usage.credits_spent: credits charged for this request. Failures are never charged and carry nousageblock.usage.balance: remaining credit balance (nullfor unlimited accounts).usage.bytes: upstream bytes this request consumed (absent on batch, which reports per-entrybytes).