Get a post’s video transcript
The captions X publishes for the video attached to a post, as one block of text plus time-coded segments. Read the coverage figure before you build on this: of 770 videos measured, 17.5% carried captions — but the average on its own misleads in both directions, so here is the spread. 41% of higher-engagement video has them, 27.5% of clips between one and three minutes, and 2.6% of clips under ten seconds. Some large accounts return nothing at all while others are near-complete, so sample the accounts you care about rather than budgeting off the average. It is not a generation delay — the same misses were re-probed 25 and 90 minutes later and were still empty — and it is not English-only: the languages X returned across the captioned set were en, pt, de, es, fr, en-US, tr, ar, he, ja, te, el, ru and it-IT. A video with no captions is a normal 200, not an error — a real one, for post 2094884934510956691, comes back as `"transcript": {"available": false, "originalLanguage": null, "durationSeconds": 191.8, "tracks": []}` with `pagesFetched: 0`. The duration is still filled in, so a miss still tells you the clip length. Do not retry it: the captions are not coming. When a transcript does exist, `tracks` holds exactly one entry — X does not translate, and none of the 770 videos declared more than one track. `transcript.originalLanguage` is inferred rather than declared, because X marks no track as the original: an auto-generated track anchors it, then the track X marks DEFAULT, then a sole track; with several uploader-supplied tracks and no DEFAULT it is null rather than a guess. Do not substitute the post’s own `lang` field for it — that describes the written text of the post and disagreed with the caption language in 28 of 92 measured cases. Billing: 1 credit.
/video-transcript Content https://api.fetchlayer.dev/twitter/video-transcript Headers
Authorization string required Bearer token — your FetchLayer API key.
Parameters
tweetId string required Numeric tweet id. Status URLs and non-numeric ids are rejected with a 400.
screenName string optional Optional handle of the post’s author. Used only to build the post URL in the response.
language string optional A BCP-47 tag to filter tracks by, e.g. en, pt or en-US. It filters rather than picks: en returns every en-* track that exists, and an exact tag wins over the broader subtag sweep. Matching is case-insensitive and falls back to the primary subtag, so de also matches a de-DE track. No translation is performed.
format string optional json or markdown. Markdown returns a rendered transcript instead of the structured record. (default: json)
Response Fields
video-transcript
Get a post’s video transcript
pagesFetched number Billing unit. 1 when a transcript came back, 0 when the video has none — the one credit every request claims is charged either way.
video.id string The post id the video belongs to.
video.url string Canonical link to the post.
video.durationMs number Length of the video in milliseconds, or null.
transcript.available boolean Whether a transcript came back. False is a normal 200, not a failure.
transcript.originalLanguage string BCP-47 tag for the language spoken in the video, or null. Inferred, not declared — see the description. Conventionally cased (language lowercase, script Titlecase, region UPPERCASE), so EN from X is normalized to en while en-US and it-IT are left alone. Compare it case-insensitively anyway.
transcript.durationSeconds number Length of the video in seconds. Filled in even when available is false.
transcript.tracks object[] One entry per caption track. Exactly one when a transcript exists, and empty otherwise — X publishes no translations, so this never held more than one entry across 770 measured videos.
transcript.tracks[].language string BCP-47 tag for this track, or "und" when X declares a track without naming its language. X is inconsistent about case — 4 of the 135 captioned videos measured came back as EN or FR — so this is normalized before you see it, and you should still compare case-insensitively.
transcript.tracks[].isOriginalLanguage boolean Whether this track is in the language actually spoken in the video.
transcript.tracks[].isAutoGenerated boolean True when X generated the captions rather than the account supplying them.
transcript.tracks[].source string How the text was obtained. Always "captions" on X.
transcript.tracks[].text string The whole track, one caption line per newline.
transcript.tracks[].segments object[] Time-coded lines, each with start, end and text, in seconds. null when a track carries no timings; X always has them, and its per-word timing data makes them unusually precise.
Request
curl -X POST "https://api.fetchlayer.dev/twitter/video-transcript" \
-H "Authorization: Bearer ss-your-key" \
-H "Content-Type: application/json" \
-d '{"tweetId":"2070270274872738258"}'Response
{
"pageTitle": "Twitter video transcript 2070270274872738258",
"responseStatus": 200,
"scrapedAt": "2026-09-16T19:13:57.396Z",
"rawIncluded": false,
"blocked": false,
"resultType": "video-transcript",
"pagesFetched": 1,
"video": {
"id": "2070270274872738258",
"url": "https://x.com/i/status/2070270274872738258",
"durationMs": 44372
},
"transcript": {
"available": true,
"originalLanguage": "en",
"durationSeconds": 44.4,
"tracks": [
{
"language": "en",
"isOriginalLanguage": true,
"isAutoGenerated": true,
"source": "captions",
"text": "In 1776, our founding fathers met in Philadelphia and changed the world forever.\nThe thing called the Declaration of Independence.\nTonight as we stand on the edge of our 250th year of independence\n…",
"segments": [
{
"start": 0,
"end": 6.419,
"text": "In 1776, our founding fathers met in Philadelphia and changed the world forever."
},
{
"start": 6.7,
"end": 10.4,
"text": "The thing called the Declaration of Independence."
},
{
"start": 10.9,
"end": 15.22,
"text": "Tonight as we stand on the edge of our 250th year of independence"
}
]
}
]
},
"raw": null
}