ScriptHaul API
Log inGet API key

Transcripts and languages

GET /v1/transcript is cache-first. Supply video_id, format, optional language, optional fallback, optional raw=1, and optional fresh=true.

Refreshing a cached transcript

A cached transcript is treated as immutable: captions rarely change after publication. When they do, add fresh=true: the transcript is fetched again from YouTube and the cached copy is replaced for every later reader. A fresh fetch is a cold delivery and costs one credit, whether or not the captions changed. Separately, an auto-generated track older than 30 days is re-checked once, in the background, on the next cache hit, at no charge, so a manual track the creator added later is picked up on its own.

curl --get https://api.scripthaul.com/v1/transcript \
  -H "Authorization: Bearer $SCRIPTHAUL_API_KEY" \
  --data-urlencode "video_id=jNQXAC9IVRw" \
  --data-urlencode "language=es" \
  --data-urlencode "fallback=true" \
  --data-urlencode "format=json"

Language contract

Situationrequesteddeliveredfallbacktranslation
Native requested trackenenfalsenot_requested
Translation servedesesfalseserved
YouTube declined the translation, translate=true, es delivered (priced below)esesfalseserved
Translation refused, fallback allowedessource languagetruerefused
Translation refused, strict modeesnonenoneretryable translation_refused error, 0 credits
No translation exists, fallback allowedessource languagetrueunavailable
No translation exists, strict modeesnonenonelanguage_unavailable error with available_languages, 0 credits

kind is manual or asr. Set fallback=false for strict mode. A labelled fallback delivery costs 1 when cold because a transcript was delivered; a strict refusal costs 0. refused is the throttle case and can succeed on a later attempt; unavailable is a fact about the video. Bulk job rows report null before delivery and never unavailable.

When YouTube declines a translation

Translation is opt-in: add translate=true (keyed MCP: translate: true). YouTube is always asked first. When it declines to translate the captions into the requested language, an opted-in request receives the transcript in that language whenever ScriptHaul can deliver it: in most languages, when the key's available balance and daily credit cap cover the price. The price is 300 credits per started 10,000 characters of transcript (at most 1,500), on top of the delivery credit, and X-Credits-Charged shows the total. The response is labelled like any served translation: delivered is the requested language and translation is served. A strict request with translate=true receives the same transcript instead of translation_refused.

Without translate=true nothing changes, and the refused rows above apply. With it, nothing extra is charged when the requested language cannot be delivered, and the same rows apply. An opted-in request never takes the labelled source copy an earlier refusal left in the cache; it asks YouTube again, while a copy already cached in the requested language stays a free hit. Every later request for that video and language is an ordinary cache hit at 0 credits, whichever key asks.

{
  "language": {
    "requested": "es",
    "delivered": "en",
    "kind": "manual",
    "fallback": true,
    "translation": "refused"
  }
}

Seven formats

Samples use a synthetic timing fixture based on the public-domain U.S. Constitution, never a customer transcript or cached YouTube transcript.

formatresponse field or raw typesample
jsoncues / JSON[{"start":0,"duration":2.4,"text":"We the People…"}]
cleantext / plain textWe the People of the United States…
timestampedtext / plain text[00:00] We the People…
bothtext / plain textclean text plus timestamped section
srttext / SubRip1 then 00:00:00,000 --> 00:00:02,400
vtttext / WebVTTWEBVTT then 00:00.000 --> 00:02.400
mdtext / Markdownheading, metadata, and timestamped cues

Use raw=1 to receive the rendered file with an attachment filename instead of the JSON wrapper.

Polling long cold calls

A cold request waits for up to 25 seconds. If work continues, it returns 202 with a request_id and relative poll_url. Repeat that URL with the same Bearer key. Settlement references make retries and polling idempotent.

Language request in four languages

Each sample requests Spanish, allows a labelled fallback, and asks for JSON. Maintained runnable versions use only standard libraries and live in the examples directory.

curl --get https://api.scripthaul.com/v1/transcript \
  -H "Authorization: Bearer $SCRIPTHAUL_API_KEY" \
  --data-urlencode "video_id=jNQXAC9IVRw" --data-urlencode "language=es" \
  --data-urlencode "fallback=true" --data-urlencode "format=json"
import os, urllib.request
url = "https://api.scripthaul.com/v1/transcript?video_id=jNQXAC9IVRw&language=es&fallback=true&format=json"
request = urllib.request.Request(url, headers={"Authorization": f"Bearer {os.environ['SCRIPTHAUL_API_KEY']}"})
print(urllib.request.urlopen(request).read().decode())
const url = new URL("https://api.scripthaul.com/v1/transcript");
url.search = new URLSearchParams({ video_id: "jNQXAC9IVRw", language: "es", fallback: "true", format: "json" });
const response = await fetch(url, { headers: { Authorization: `Bearer ${process.env.SCRIPTHAUL_API_KEY}` } });
console.log(await response.json());
url := "https://api.scripthaul.com/v1/transcript?video_id=jNQXAC9IVRw&language=es&fallback=true&format=json"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("SCRIPTHAUL_API_KEY"))
response, err := http.DefaultClient.Do(req)
if err != nil { log.Fatal(err) }
defer response.Body.Close()
io.Copy(os.Stdout, response.Body)