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
| Situation | requested | delivered | fallback | translation |
|---|---|---|---|---|
| Native requested track | en | en | false | not_requested |
| Translation served | es | es | false | served |
YouTube declined the translation, translate=true, es delivered (priced below) | es | es | false | served |
| Translation refused, fallback allowed | es | source language | true | refused |
| Translation refused, strict mode | es | none | none | retryable translation_refused error, 0 credits |
| No translation exists, fallback allowed | es | source language | true | unavailable |
| No translation exists, strict mode | es | none | none | language_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.
| format | response field or raw type | sample |
|---|---|---|
json | cues / JSON | [{"start":0,"duration":2.4,"text":"We the People…"}] |
clean | text / plain text | We the People of the United States… |
timestamped | text / plain text | [00:00] We the People… |
both | text / plain text | clean text plus timestamped section |
srt | text / SubRip | 1 then 00:00:00,000 --> 00:00:02,400 |
vtt | text / WebVTT | WEBVTT then 00:00.000 --> 00:02.400 |
md | text / Markdown | heading, 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)