Fetch a YouTube video and get its transcript back as Markdown, with title, description, chapters, and timestamps.
Agents get sent to YouTube too. A conference talk, a tutorial, a product demo. The watch page itself is a player, a sidebar of recommendations, and a comment thread. Your agent needs what was said.
Fetch recognizes YouTube video URLs automatically. Send the video URL and get the transcript back as Markdown, with title, channel, description, and chapters, instead of rendering the watch page.
import { ExpandClient } from '@expandai/sdk'
const expand = new ExpandClient()
const markdown = await expand.fetch({ url: 'https://www.youtube.com/watch?v=aBcDeFgHiJk' })expandai fetch// Arguments for the fetch tool
{
"url": "https://www.youtube.com/watch?v=aBcDeFgHiJk"
}curl https://api.expand.ai/v1/fetch \
-H "x-expand-api-key: $EXPAND_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://www.youtube.com/watch?v=aBcDeFgHiJk"}'Any YouTube URL associated with one video is converted to transcript Markdown. Everything else is fetched as an ordinary page.
youtube.com, www.youtube.com, m.youtube.com, music.youtube.com, and youtube-nocookie.com are all acceptable. Share noise in the query string (si, t, feature, a playlist list next to a v) is ignored. The video ID is what matters.
Playlists, channels, @handle pages, and search results carry no single video ID, so they render in a browser and come back as a page. See Browser-rendered pages.
| Form | Transcript | Example |
|---|---|---|
| Watch page | Yes | https://www.youtube.com/watch?v=aBcDeFgHiJk |
| Short link | Yes | https://youtu.be/aBcDeFgHiJk |
| Shorts | Yes |
A video comes back in either of Fetch's two modes, as Markdown text or as a JSON object. See Two modes.
Markdown Mode returns the transcript as Markdown text. The frontmatter has the same keys as any other page fetched as Markdown text, so no additional parsers are required.
import { ExpandClient } from '@expandai/sdk'
const expand = new ExpandClient()
const markdown = await expand.fetch({ url: 'https://www.youtube.com/watch?v=aBcDeFgHiJk' })expandai fetch https://www.youtube.com/watch?v=aBcDeFgHiJkcurl https://api.expand.ai/v1/fetch \
-H "x-expand-api-key: $EXPAND_API_KEY" \
-H "Content-Type: application/json"
---
url: "https://www.youtube.com/watch?v=aBcDeFgHiJk"
capturedAt: "2026-09-08T10:15:00.000Z"
pageType: "video"
title: "Keynote: What changed in the browser this year"
author: "Example Conf"
createdAt: "2026-05-14"
image: "https://i.ytimg.com/vi/aBcDeFgHiJk/maxresdefault.jpg"
---
# Keynote: What changed in the browser this year
Example Conf · 41:07 · Published 2026-05-14 · 128,304 views
Transcript: captions (manual, en)
## Description
Opening keynote from Example Conf 2026. Slides: https://example.com/slides
## Transcript
### [0:00]
Reading it top to bottom:
Transcript: line. Where the text came from, such as captions (manual, en). See Transcription waterfall.### [m:ss] Title headings, only when the uploader set chapters.[0:42] or [1:02:03]. Sound-only captions like [Music] are dropped.JSON Mode returns the transcript as a JSON object. It has all the standard fields plus data.video, which holds the video's details as fields, so you don't have to pull them out of the Markdown.
import { ExpandClient } from '@expandai/sdk'
const expand = new ExpandClient()
const page = await expand.fetchJson({ url: 'https://www.youtube.com/watch?v=aBcDeFgHiJk' })expandai fetch https://www.youtube.com/watch?v=aBcDeFgHiJk --format json// Arguments for the fetch tool
{
"url": "https://www.youtube.com/watch?v=aBcDeFgHiJk"
{
"meta": {
"version": 1,
"url": "https://www.youtube.com/watch?v=aBcDeFgHiJk",
"capturedAt": "2026-09-08T10:15:00.000Z",
"snapshotId": "0f9c3a1e-...",
"contentType":
| Field | Definition |
|---|---|
data.video.videoId | The eleven-character ID, whichever URL form you sent. |
data.video.title, channel, channelUrl | As YouTube reports them. channel and channelUrl are absent when unknown. |
data.video.durationSeconds | Length of the video, not of the transcript. |
json comes back as an empty list, and the other fields are left out.To watch the video while you read the transcript, open the Fetch in the Playground. It shows the transcript next to an embedded player of the video.
Fetch uses existing captions first and transcribes the audio only when there are none. It tries, in order:
Select one of the above steps to see how it is returned.
"data": {
"video": {
"videoId": "aBcDeFgHiJk",
"language": "de",
"transcript": {
"source": "captions",
"kind": "manual",
"language": "de"
}
},
"usage": { "videos": 1, "audioSeconds": 0 }
}# Keynote: What changed in the browser this year
Example Conf · 41:07 · Published 2026-05-14 · 128,304 views
Transcript: captions (manual, de)Both modes say where the transcript came from: the Transcript: line in Markdown text, and data.video.transcript in the JSON object.
For a German video, the first two steps return source: "captions", language: "de", and kind: "manual" or "auto". English captions use language: "en"; another uploaded track uses its own language. If Fetch transcribes the audio, it returns source: "transcription" with no kind, and data.usage.audioSeconds records the transcribed duration instead of 0.
| Field | Values | Definition |
|---|---|---|
source | captions or transcription | Whether Fetch used a caption track from YouTube or transcribed the audio with a speech model. |
kind | manual or auto | When is , says whether the track is the creator's own () or YouTube's speech recognition (). |
Fetching a YouTube video costs 6 credits ($0.006), whatever its length. That's 1 credit for the Fetch request plus 5 credits for the video, which cover the transcript, timestamps, chapters, and preview frames.
When a video has no caption track, Fetch transcribes the audio with a speech model, which charges an additional 10 credits per minute:
| Unit | Credits | When it applies |
|---|---|---|
| Video | 5 per video | Every delivered transcript. One per Fetch, whatever its length. |
| Audio | 10 per minute | Only when no caption track exists. Rounded up, one-minute minimum. |
For example, a 25-minute video with captions costs 6 credits. The same video with no captions costs 256 credits.
With captions
No captions
You are only charged for a delivered result, never when a video can't be fetched, such as a private video or one longer than maxAudioSeconds. See Video errors.
data.usage reports videos and audioSeconds, so you can check how each Fetch was charged. See Pricing & Usage.
When a video has no captions, Fetch transcribes the audio with a speech model. That's the only cost that grows with the video's length. You can cap it with the optional documentConfig.maxAudioSeconds field.
| Field | Definition | Type | Default | Range |
|---|---|---|---|---|
maxAudioSeconds | Refuse the Fetch rather than transcribe a video longer than this. | integer | 3600 | 1 to 14400 |
import { ExpandClient } from '@expandai/sdk'
const expand = new ExpandClient()
const markdown = await expand.fetch({
url: 'https://youtu.be/aBcDeFgHiJk',
documentConfig: { maxAudioSeconds: 900 },
})curl https://api.expand.ai/v1/fetch \
-H "x-expand-api-key: $EXPAND_API_KEY" \
-H "Content-Type: application/json" \
maxAudioSeconds. A video of any length comes back in full for the flat 6 credits, even a six-hour livestream replay.maxAudioSeconds before it downloads any audio. If the video is longer than the limit, the request is refused and you aren't charged.14400 seconds).A transcription that is within the limit can still take a while.
Batched Fetch doesn't return YouTube transcripts yet. It renders the watch page in a browser and returns it as a standard page, with no transcript. Send video URLs to POST /v1/fetch or POST /v1/fetch/json until this lands.
On top of the standard Fetch errors, a video Fetch can return three more errors. Their full payloads are in the API reference.
_tag | Code | Retryable |
|---|---|---|
FetchVideoUnavailable | 422 | Sometimes |
FetchDocumentProcessingUnavailable | 503 | Yes |
FetchVideoUnavailableYouTube won't serve this video, so there's no transcript to return.
{
"_tag": "FetchVideoUnavailable",
"url": "https://www.youtube.com/watch?v=aBcDeFgHiJk",
"availability": "live",
"message": "This live event will begin in 2 hours"
}The availability field explains why the video is unavailable, and whether it's retryable:
| Value | Definition | Retryable |
|---|---|---|
private | Private. | No |
removed | Deleted, or its account was terminated. | No |
geoRestricted | Not available from Expand's region. |
This error is not retryable, except when availability is live or upcoming. Fetch those again after the stream ends, once YouTube has processed the replay.
FetchDocumentProcessingUnavailableYouTube refused the request, or the speech model couldn't finish.
{
"_tag": "FetchDocumentProcessingUnavailable",
"url": "https://www.youtube.com/watch?v=aBcDeFgHiJk",
"operation": "video",
"message": "YouTube refused the request"
}The operation field is set to video when YouTube refused the request, such as a bot check or a rate limit, and transcription when the speech model couldn't finish.
The TypeScript SDK retries this error automatically, with backoff.
FetchPageTooLargeThe video has no captions and is longer than maxAudioSeconds.
{
"_tag": "FetchPageTooLarge",
"url": "https://www.youtube.com/watch?v=aBcDeFgHiJk",
"limitKind": "audio_seconds",
"limit": 3600,
"measured": 5400
}For a video, the limitKind field is set to audio_seconds, and limit and measured are in seconds.
To fetch the video, increase the maxAudioSeconds limit, up to the ceiling of four hours (14400 seconds). A video longer than that can't be transcribed. See Limiting long transcriptions.
https://www.youtube.com/shorts/aBcDeFgHiJk |
| Live replay | Yes | https://www.youtube.com/live/aBcDeFgHiJk |
| Embed | Yes | https://www.youtube.com/embed/aBcDeFgHiJk |
| Legacy player | Yes | https://www.youtube.com/v/aBcDeFgHiJk |
| Playlist | No | https://www.youtube.com/playlist?list=PLaBcDeFgHiJk |
| Channel | No | https://www.youtube.com/@exampleconf |
| Search results | No | https://www.youtube.com/results?search_query=keynote |
curl https://api.expand.ai/v1/fetch/json \
-H "x-expand-api-key: $EXPAND_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://www.youtube.com/watch?v=aBcDeFgHiJk"}'data.video.publishedAt | Upload date as an ISO calendar date. |
data.video.viewCount | View count at capture time. |
data.video.language | The video's primary spoken language, when YouTube or the transcript reports one. |
data.video.transcript | source, kind, and language of the transcript. See Transcription waterfall. |
data.video.storyboard | Preview frames sampled through the video, as sprite sheet images. Absent when YouTube doesn't publish them. |
data.document.url | A presigned link to what the transcript was made from: the caption track as JSON, or the audio that was transcribed. |
data.usage.audioSeconds | Seconds of audio transcribed. 0 on the captions path. |
meta.url | Always the canonical watch?v= URL. |
meta.contentType | text/html, because that is what YouTube served. The transcript is Expand's rendering of it, the same way a PDF's Markdown is. |
sourcecaptionsmanualautolanguage | A BCP-47 language tag (en, de, pt-BR) | The transcript's language, when known. |
FetchPageTooLarge | 413 | No |
| No |
ageRestricted | Behind an age gate that needs a signed-in account. | No |
membersOnly | Reserved for channel members or Premium subscribers. | No |
live | Streaming right now. There is no transcript until it ends. | Yes |
upcoming | A scheduled premiere or stream that has not started. | Yes |
unknown | Withheld for a reason YouTube did not name. | No |