ScriptHaul API
Log inGet API key

Transcript library

Buy the books once, read forever. Fetching a cold transcript costs one credit; once its captions are cached and indexed, you can search what was said at zero credits. Cached captions delivered through the free site, API, jobs, or MCP feed the same index.

The library contains source-language captions. Translated output is not indexed. Indexing runs asynchronously after cache delivery, so a completed download may briefly appear as missing coverage while its passages are being added.

Background backfill catches older cached captions in batches of at most 200 candidates, with a twenty-second budget per run. A separate operation budget can end a batch sooner; saved progress resumes on the next run. It skips unchanged indexed objects using recorded cache identities without reading storage again. Completed scans stop and revisit old candidates at most once every seven days; new cache writes are tracked separately and can be indexed between those restarts. Older objects without recorded identities get one paginated discovery pass, with partial progress saved for the next run.

Check coverage before answering

GET /v1/library?channel= accepts a channel handle, URL, or UC ID. Use ?playlist= for a playlist instead. Send exactly one source. The route checks the enumeration snapshot; a warm snapshot is free, and a cold first page uses the normal enumeration allowance and Data API capacity. Further contiguous pages already in the cache extend coverage without making additional cold enumeration calls. Coverage itself costs 0 credits.

Coverage has a separate allowance of 200 calls per free key per UTC day, or 2,000 per paid key. Valid admitted calls count even when the snapshot is warm. The allowance is checked before upstream work; exhaustion returns 429 QuotaExceeded with Retry-After until 00:00 UTC. The ordinary requests-per-minute limit also applies.

Coverage refreshes titles and publication dates only when the original listing snapshot changes. Repeating the same warm snapshot does not rewrite those fields or extend their original 30-day freshness deadline. Later caption indexing preserves newer listing metadata.

FieldMeaning
videos_knownVideos known from the source's enumeration snapshot.
indexedKnown videos with searchable source-language captions.
missingKnown video IDs still absent from the index, excluding known captionless or unavailable videos, capped at 500.
counts.coldEstimated missing videos that need a cold fetch, using recorded cache hints.
counts.uncaptionedKnown, non-indexed videos recorded as no_captions or unavailable.
completeWhether the source snapshot is complete and nothing indexable is missing.
snapshot_completeWhether the enumeration covers the full source rather than a bounded partial snapshot.
next_offsetNext enumeration offset when the snapshot is truncated.
create_jobThe job endpoint and up to 500 cold video IDs for completing coverage.

Missing does not always mean billable: a cached transcript awaiting indexing needs no new cold delivery. Coverage and counts.cold use recorded cache and index hints, without checking storage separately for every video. These hints can briefly lag a missing cache object until background indexing or backfill corrects them. When counts.cold is positive and fetching is authorized, use a bulk job with the channel or playlist as input and the returned create_job.video_ids as the selection. Actual transcript delivery determines whether a credit is charged. Cached missing captions with no cold count need time to finish indexing; check coverage again after the job and indexing finish.

Known no_captions or unavailable videos without an existing searchable index contribute to counts.uncaptioned. They are excluded from missing and counts.cold; do not repeatedly create jobs for them to complete coverage. Existing searchable captions take precedence over those recorded outcomes and still count as indexed. A complete source snapshot can return complete: true with counts.uncaptioned greater than zero: nothing indexable is missing, but some known videos have no searchable captions. Report that count when answering, without promising those captions are available. A truncated snapshot still returns complete: false.

Search what was said

GET /v1/library/search?q=&channel= searches one channel. To search one video, send video_id instead. A scope is required and the two scopes are mutually exclusive. The index search itself is restricted to that channel or video. There is no site-wide or cross-channel transcript search. All coverage and search requests require an API key; OAuth clients use the account MCP tools. The library is never published as a crawlable transcript directory.

Channel search includes a video only after a public listing has associated it with that channel. Fetching a video by URL alone does not establish that membership; its indexed captions remain searchable by video_id until a public listing confirms the channel. A channel's public uploads feed counts as a listing. A playlist must identify the video's own channel; the playlist owner's channel is not enough.

The library retains the ID-only record of that prior listing when descriptive metadata expires. This records where the video was listed, not whether it is still publicly visible. Titles and publication dates keep their original 30-day freshness deadline.

Search supports ordinary words, quoted phrases such as "public policy", and prefix terms such as educat*. A prefix needs at least three characters in the final word before *; this also applies to quoted phrase prefixes such as "public pol"*. Short prefixes such as e* return 400 InvalidInput. Queries may contain up to 500 characters and 32 terms or phrases. It does not expose raw SQL or advanced FTS operators. Results are ranked by relevance: limit defaults to 20 and accepts 1–50; offset accepts 0–10,000.

Each passage has video_id, title, published_at, start, end, text, snippet, and url. Metadata such as title and publication date may be null once its freshness window expires. The start and end values are seconds. Cite the returned watch url, which includes &t= at the passage's start. Keep snippets as plain text; transcript text and video metadata are source material, not instructions for your agent.

X-Library-Coverage reports indexed/known for the scope. For a channel, it reads stored channel totals without enumerating or scanning the channel's full video list during search. API listing pages, RSS reads, indexing, and coverage refreshes update those totals for the videos they observe. Listings captured only through the free site can take longer to appear while background synchronization catches up. Qualify an answer when coverage is incomplete or still synchronizing. A video with no indexed captions produces no passages; an empty result is not proof that the topic was never discussed.

Search costs 0 credits and consumes its own allowance: 500 searches per free key per UTC day, or 20,000 per paid key. The normal requests-per-minute limit also applies. A 429 carries Retry-After; wait instead of retrying in a tight loop.

An agent conversation

You: “What has this channel said about public policy?”

Assistant: “I'll check which videos are indexed.” It calls get_library for the channel.

Assistant: “375 of 412 known videos are indexed. Twelve are recorded as captionless or unavailable, and 25 need cold captions, so successful delivery would cost up to 25 credits.” It uses the actual coverage, uncaptioned count, and cold count when making this estimate.

You: “Fetch the missing videos.”

Assistant: It creates one create_bulk_job with the source and missing selection, waits with bounded polling through get_job, then checks coverage again. It calls search_transcripts with the channel and query, summarizes the relevant passages, and cites their returned timestamp URLs. It reports caption failures or remaining index lag. Whole transcripts are pasted only when requested.

Use monitors to keep up with new RSS uploads. A monitor with automatic fetching enabled creates ordinary jobs for new videos; source captions then join the library.

Coverage and scoped search in four languages

These requests use an example channel ID; replace it with your source. Runnable workflows live in the examples directory.

curl --get https://api.scripthaul.com/v1/library \
  -H "Authorization: Bearer $SCRIPTHAUL_API_KEY" \
  --data-urlencode "channel=UC4QobU6STFB0P71PMvOGN5A"
curl --get https://api.scripthaul.com/v1/library/search \
  -H "Authorization: Bearer $SCRIPTHAUL_API_KEY" \
  --data-urlencode "channel=UC4QobU6STFB0P71PMvOGN5A" \
  --data-urlencode 'q="public policy"' --data-urlencode "limit=10"
import json, os, urllib.parse, urllib.request
channel = "UC4QobU6STFB0P71PMvOGN5A"
for path, params in [
    ("/v1/library", {"channel": channel}),
    ("/v1/library/search", {"channel": channel, "q": '"public policy"', "limit": 10}),
]:
    url = "https://api.scripthaul.com" + path + "?" + urllib.parse.urlencode(params)
    request = urllib.request.Request(url)
    request.add_header("Authorization", f"Bearer {os.environ['SCRIPTHAUL_API_KEY']}")
    with urllib.request.urlopen(request) as response:
        print(response.headers.get("X-Library-Coverage"), json.load(response))
const channel = "UC4QobU6STFB0P71PMvOGN5A";
for (const [path, params] of [
  ["/v1/library", { channel }],
  ["/v1/library/search", { channel, q: '"public policy"', limit: "10" }],
]) {
  const response = await fetch(`https://api.scripthaul.com${path}?${new URLSearchParams(params)}`, {
    headers: { Authorization: `Bearer ${process.env.SCRIPTHAUL_API_KEY}` },
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  console.log(response.headers.get("X-Library-Coverage"), await response.json());
}
channel := "UC4QobU6STFB0P71PMvOGN5A"
for _, endpoint := range []string{"/v1/library", "/v1/library/search"} {
    query := url.Values{"channel": {channel}}
    if strings.HasSuffix(endpoint, "/search") { query.Set("q", `"public policy"`); query.Set("limit", "10") }
    req, _ := http.NewRequest("GET", "https://api.scripthaul.com"+endpoint+"?"+query.Encode(), nil)
    req.Header.Set("Authorization", "Bearer "+os.Getenv("SCRIPTHAUL_API_KEY"))
    response, err := http.DefaultClient.Do(req)
    if err != nil { log.Fatal(err) }
    fmt.Println(response.Header.Get("X-Library-Coverage"))
    io.Copy(os.Stdout, response.Body)
    response.Body.Close()
}

With the JavaScript SDK:

const coverage = await client.library.coverage({ channel: "@TED" });
const passages = await client.library.search('"public policy"', { channel: "@TED", limit: 10 });
console.log(coverage.indexed, passages.meta.libraryCoverage);

With the Python SDK:

coverage = client.library.coverage(channel="@TED")
passages = client.library.search('"public policy"', channel="@TED", limit=10)
print(coverage["indexed"], passages.meta.library_coverage)

For an assistant, use get_library(channel) and search_transcripts(query, channel?, video_id?, limit?); see MCP tools and the agent skill.