Expand AI logo
DocsDocs
Glow Active
API Reference
Login

Documentation

Get Started

OverviewMCP ServerQuickstartWhy ExpandWays to Use Expand

Fetch

OverviewHow Fetch WorksOutput ModelProgressive DisclosureInclude OptionsDocumentsYouTubeBrowser BehaviorHighlightsPlayground & ReplayBatched Fetch

Reference

MCP Tools & ResourcesCLI, Skill & HooksCLI CommandsAPI ReferenceTypeScript SDK

Account & Billing

Pricing & UsageTiersFAQ

Machine-Readable Docs

start.mdllms.txtllms-full.txtDocs as Markdown
Browse docs

Get Started

OverviewMCP ServerQuickstartWhy ExpandWays to Use Expand

Fetch

OverviewHow Fetch WorksOutput ModelProgressive DisclosureInclude OptionsDocumentsYouTubeBrowser BehaviorHighlightsPlayground & ReplayBatched Fetch

Reference

MCP Tools & ResourcesCLI, Skill & HooksCLI CommandsAPI ReferenceTypeScript SDK

Account & Billing

Pricing & UsageTiersFAQ

Machine-Readable Docs

start.mdllms.txtllms-full.txtDocs as Markdown

YouTube

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. What your agent needs is what was said.

Fetch returns the transcript on the endpoint you already call. Send the video URL and get the transcript back. POST /v1/fetch recognises a single-video YouTube URL and returns the video's transcript as Markdown, with title, channel, description, and chapters, instead of rendering the watch page.

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"}'

Which URLs are recognised

Any URL that names one video takes the transcript route. Everything else on YouTube is fetched as an ordinary page.

FormExample
Watch pagehttps://www.youtube.com/watch?v=aBcDeFgHiJk
Short linkhttps://youtu.be/aBcDeFgHiJk
Shortshttps://www.youtube.com/shorts/aBcDeFgHiJk
Live replay

youtube.com, www.youtube.com, m.youtube.com, music.youtube.com, and youtube-nocookie.com all count. Share noise in the query string (si, t, feature, a playlist list next to a v) is ignored, and 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 Behavior.

What comes back

POST /v1/fetch returns the transcript as one Markdown document. The front matter uses the same keys a page fetch does, so nothing downstream needs a new parser.

---
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:

  • Summary line. Channel, length, publish date, and view count at the time of the fetch. Fields YouTube did not report are left out rather than filled with placeholders.
  • Transcript: line. Where the text came from. captions (manual, en), captions (auto-generated, en), or transcription (Deepgram, en). See Captions or transcription.
  • Description. The uploader's description, links intact. Absent when the video has none.
  • Chapters. ### [m:ss] Title headings appear only when the uploader set chapters. Every chapter is kept, even one nobody speaks in, so the outline survives in full.
  • Paragraphs. Each paragraph starts with the time it begins, [0:42] under an hour and [1:02:03] past it. Cues are merged into paragraphs at pauses and sentence ends, so the token count stays close to the plain transcript. Sound-only captions such as [Music] or [Applause] are dropped.

JSON mode

POST /v1/fetch/json returns the same Markdown plus a typed data.video block, so a JSON-native caller does not parse the front matter or the Transcript: line back out.

{
  "meta": {
    "version": 1,
    "url": "https://www.youtube.com/watch?v=aBcDeFgHiJk",
    "capturedAt": "2026-09-08T10:15:00.000Z",
    "snapshotId": "0f9c3a1e-...",
    "contentType": 























FieldMeaning
data.video.videoIdThe eleven-character id, whichever URL form you sent.
data.video.title, channel, channelUrlAs YouTube reports them. channel and channelUrl are absent when unknown.
data.video.durationSecondsLength of the video, not of the transcript.

State JSON, Appendix, links, screenshots, and browser replay exist only because a browser rendered something, so a video fetch has none of them. Asking for them does not fail the request. The fields are simply absent. The playground shows the transcript next to the embedded player.

Captions or transcription

Fetch prefers text that already exists over text it has to generate. It tries, in order:

  1. The creator's own captions in the video's language.
  2. YouTube's auto-generated captions in that language.
  3. The creator's captions in English, then YouTube's auto-generated English captions.
  4. Any other captions the creator uploaded.

Only when none of those exists does Fetch download the audio and transcribe it with a speech model.

The Transcript: line and data.video.transcript tell you which you got:

sourcekindWhat it means
captionsmanualThe creator uploaded or corrected this track. The most faithful text on offer.
captionsautoYouTube's own speech recognition. Usually unpunctuated and occasionally wrong on names and numbers.

language is a BCP-47 tag (en, de, pt-BR) when known. There is no option to request a specific language yet. A video in German with German captions returns German, because a translated auto-generated track is the least faithful text available.

Long videos

Transcription is the only step that costs more than the fetch, and documentConfig.maxAudioSeconds bounds it. Every field is optional.

FieldTypeDefaultRangeWhat it does
maxAudioSecondsinteger36001 to 14400Refuse the fetch rather than transcribe a video longer than this.

Fetch checks the video's length against the limit before it downloads a byte of audio, so a refusal costs you nothing. The captions path ignores the limit entirely. A six-hour livestream replay with captions comes back in full for the flat fetch price.

curl https://api.expand.ai/v1/fetch \
  -H "x-expand-api-key: $EXPAND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://youtu.be/aBcDeFgHiJk",
    "documentConfig": { "maxAudioSeconds": 900 }
  }'

Raise the limit only as far as the videos you expect. Four hours is the ceiling, and transcription runs inside the same 150-second request budget as every fetch, so a video near the ceiling can run out of time and return 504 FetchCaptureTimeout.

Pricing

A video costs 6 credits, whatever its length. That is the one-credit Fetch plus a five-credit video unit, and it covers the transcript, the timestamps, the chapters, and the frames. 1,000 credits cost $1, so six credits is under a cent.

When a video has no caption track we transcribe the audio, which charges minutes on top:

UnitCreditsWhen it applies
Video5 per videoEvery delivered transcript. One per fetch, whatever the runtime.
Audio10 per minuteOnly when no caption track exists. Rounded up, one-minute minimum.

A 25-minute video with captions costs 6 credits. The same video with no captions costs 6 + 25 × 10 = 256 credits. Refused fetches cost nothing at all, and data.usage reports videos and audioSeconds so you can reconcile the charge against what you got. See Pricing & Usage.

Errors

A video fetch returns three statuses a page fetch never does:

_tagCodeMeaning
FetchVideoUnavailable422YouTube will not serve this video to anyone in our position. availability says why. Not retryable.
FetchDocumentProcessingUnavailable503 means YouTube refused our request, a bot check or a rate limit. means the speech provider could not finish. Both are retryable.

availability on a 422 is one of:

availabilityThe video is
privatePrivate.
removedDeleted, or its account was terminated.
geoRestrictedNot available from Expand's region.
ageRestricted
{
  "_tag": "FetchVideoUnavailable",
  "url": "https://www.youtube.com/watch?v=aBcDeFgHiJk",
  "availability": "live",
  "message": "This live event will begin in 2 hours"
}

A live or upcoming stream is worth fetching again after it ends, once YouTube has processed the replay. The other reasons do not change on retry.

YouTube answers 200 for all of these, so a video fetch never returns 502 FetchOriginHttpError. The refusal is a fact about the video, not about the HTTP exchange, and 422 says so. The Errors reference has the full list and the payload shapes.

Batched Fetch

Batched Fetch does not take the transcript route yet. A YouTube URL in a batch renders the watch page in a browser and comes back as a page. Send video URLs to POST /v1/fetch or POST /v1/fetch/json until this lands.

Next steps

  • Documents: PDFs, images, office files, ebooks, and audio on the same endpoint.
  • Include Options: choose what a fetch returns.
  • Playground & Replay: read a transcript next to the video it came from.
  • Pricing & Usage: see what transcribed minutes cost in credits.
  • Errors: handle 413, 422, and 503 in your client.
PreviousDocuments
NextBrowser Behavior

On This Page

Which URLs are recognisedWhat comes backJSON modeCaptions or transcriptionLong videosPricingErrorsBatched FetchNext steps
https://www.youtube.com/live/aBcDeFgHiJk
Embedhttps://www.youtube.com/embed/aBcDeFgHiJk
Legacy playerhttps://www.youtube.com/v/aBcDeFgHiJk
Welcome
[0:04] Good morning, everyone. Thanks for being here so early on a Thursday.
[0:21] I want to start with a number. Last year, 38 percent of the pages we measured shipped a table the crawler never saw.
### [2:15] The rendering gap
[2:15] Here is what that looks like in practice. ...
"text/html"
},
"markdown": "---\nurl: \"https://www.youtube.com/watch?v=aBcDeFgHiJk\"\n...",
"json": [],
"data": {
"video": {
"provider": "youtube",
"videoId": "aBcDeFgHiJk",
"title": "Keynote: What changed in the browser this year",
"channel": "Example Conf",
"channelUrl": "https://www.youtube.com/@exampleconf",
"durationSeconds": 2467,
"publishedAt": "2026-05-14",
"viewCount": 128304,
"language": "en",
"transcript": { "source": "captions", "kind": "manual", "language": "en" }
},
"document": {
"url": "https://s3.amazonaws.com/.../document/original.json?X-Amz-Signature=...",
"contentType": "application/json",
"sizeBytes": 48211
},
"usage": { "documentPages": 0, "ocrPages": 0, "audioSeconds": 0 }
}
}
data.video.publishedAtUpload date as an ISO calendar date.
data.video.viewCountView count at capture time.
data.video.languageThe video's primary spoken language, when YouTube or the transcript reports one.
data.video.transcriptsource, kind, and language of the transcript. See below.
data.document.urlA presigned link to what the transcript was made from: the caption track as JSON, or the audio that was transcribed.
data.usage.audioSecondsSeconds of audio transcribed. 0 on the captions path.
meta.urlAlways the canonical watch?v= URL. The URL you sent is data.response.requestedUrl when it differs.
meta.contentTypetext/html, because that is what YouTube served. The transcript is Expand's rendering of it, the same way a PDF's Markdown is.
transcriptionabsentNo caption track existed. Expand transcribed the audio. Punctuated, paragraphed, language detected.
operation: "video"
operation: "transcription"
FetchPageTooLarge413limitKind: "audio_seconds" means the video is longer than maxAudioSeconds. limit and measured are in seconds.
Behind an age gate that needs a signed-in account.
membersOnlyReserved for channel members or Premium subscribers.
liveStreaming right now. There is no transcript until it ends.
upcomingA scheduled premiere or stream that has not started.
unknownWithheld for a reason YouTube did not name.