The Scriptube API lets you submit YouTube video, playlist, or channel URLs and get back transcripts and metadata as JSON, CSV, or JSONL, plus timed segments through the API or baked into SRT/VTT subtitle files. This guide covers everything: authentication, submitting a batch, polling for results, exporting, and handling errors â with working curl and Python examples against https://scriptube.me.
Who this is for
The API is available on Pro and above. If you're looking for the dashboard workflow instead, see getting started with your first batch. If you just need one transcript right now with no code, use the free transcript tool.
Authentication
Generate a key from the dashboard (or via the keys API) and send it on every request as a header:
X-API-Key: sk_live_your_key_here
Every /api/* error â bad key, missing scope, bad input â comes back in one envelope:
{"code": "invalid_api_key", "message": "...", "detail": "..."}
Keys carry one of two scopes: read can poll, export, and list â it cannot submit a batch or touch billing. full can do everything, including submit, retry, cancel, and manage webhooks. If a read-only key tries to call /resolve or POST /transcripts, you'll get a 403, not a billing surprise. See key scopes, rotation, and revocation for creating and rotating keys programmatically.
Two ways to submit a batch
Scriptube bills "fetch once, sell many times": a transcript you've already processed is instant and cheap to serve again. The API has two submit endpoints that both exploit that:
| Endpoint | Behavior |
|---|---|
POST /api/v1/transcripts | Always async. Returns 202 with a batch_id immediately; you poll or wait for a webhook. |
POST /api/v1/transcripts/resolve | Hybrid. Videos already stored come back inline in the same response (no polling). Anything new still gets a batch_id to poll. |
Use /resolve when you want instant answers for cache hits. Use the plain submit endpoint when you always want the same async shape, or see webhooks vs. polling for how to avoid polling loops entirely.
Submit example
curl -s -X POST https://scriptube.me/api/v1/transcripts \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"urls": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"],
"language": "en",
"translate_to_english": false
}'
Response (202):
{
"batch_id": "b2b1a7b0-...",
"batch_number": 142,
"status": "queued",
"url_count": 1,
"message": "Batch queued",
"batches": [{"kind": "videos", "batch_id": "b2b1a7b0-...", "status": "queued", "url_count": 1}]
}
status can already be completed in the same response if every video was already stored â mixed batches of videos, playlists, and channels are supported too; see mixing inputs in one request.
Useful body fields: urls (required), language (default en), translate_to_english, external_id / metadata / callback_url for tagging and routing your own systems â covered in tagging batches with external_id and metadata.
urls accepts video links, playlist links, and channel links (or @handles) in any combination, plus bare 11-character video ids. A playlist or set of videos becomes one ordinary batch; each distinct channel becomes its own background batch, and every batch the request creates comes back in the batches array, sharing one submission_id. Up to 100 entries per request, with a cap of 10 playlist/channel links (each one costs a YouTube API call to expand).
Polling a batch
curl -s https://scriptube.me/api/v1/transcripts/b2b1a7b0-... \
-H "X-API-Key: YOUR_API_KEY"
| Query param | What it does |
|---|---|
include_metadata | Adds title, channel, view/like counts, description, tags, etc. |
include_segments | Adds timecoded {text, start, duration} segments (implies metadata). |
include_languages | Adds every stored caption track per item, not just the primary one. |
language | Which caption track to serve for this read (falls back to the batch default if missing). |
skip / limit | Paginate items â see pagination for large batches. |
Each item carries a status (completed, failed, skipped, âĻ), the transcript text, and â since 0070 â available_languages, requested_language, served_language, and language_hint, so you always know exactly which language you got.
Exporting results
curl -s "https://scriptube.me/api/v1/transcripts/b2b1a7b0-.../export?format=srt" \
-H "X-API-Key: YOUR_API_KEY" \
-o transcript.srt
format is one of json, csv, txt, jsonl, srt, or vtt. include_failed defaults to true so failed/skipped items ship as rows with a status and error_code instead of silently disappearing; set it to false for completed-only exports. partial=true lets you export a batch that's still running. See the full export schema reference for exact column names, and timestamps across every format for SRT/VTT specifics.
A full workflow in Python
import time
import requests
API_KEY = "YOUR_API_KEY"
BASE = "https://scriptube.me"
HEADERS = {"X-API-Key": API_KEY}
# 1. Submit a batch (always async: 202 + batch_id)
resp = requests.post(
f"{BASE}/api/v1/transcripts",
headers=HEADERS,
json={"urls": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"], "language": "en"},
)
resp.raise_for_status()
batch = resp.json()
batch_id = batch["batch_id"]
print("submitted:", batch_id, batch["status"])
# 2. Poll until the batch leaves "queued"/"processing"
while True:
poll = requests.get(f"{BASE}/api/v1/transcripts/{batch_id}", headers=HEADERS)
poll.raise_for_status()
status = poll.json()["status"]
if status in ("completed", "failed", "partial", "cancelled"):
break
time.sleep(5)
print("final status:", status)
# 3. Export the finished batch as SRT
export = requests.get(
f"{BASE}/api/v1/transcripts/{batch_id}/export",
headers=HEADERS,
params={"format": "srt"},
)
export.raise_for_status()
with open("transcript.srt", "wb") as f:
f.write(export.content)
For a shorter starting point and pagination, see the Python tutorial.
Errors and rate limits
Every error is {"code", "message", "detail"} â branch on code, not the message text. The ones worth handling explicitly:
| Status / code | Meaning |
|---|---|
402 | Not enough credits to cover the batch. |
429 rate_limited | Per-minute throttle exceeded â honor Retry-After. |
409 elevenlabs_disabled | A retry asked to force AI transcription, which is off (AI transcription for caption-less videos is coming soon, not available today). |
Writes (submit, /resolve, lifecycle actions) are limited to 30 requests/minute; reads (polling, exports, /library, etc.) to 60/minute. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset so you can back off before you get throttled. Rate limits are counted per API key, so two integrations sharing one office's egress IP don't throttle each other â only two scripts sharing one key do.
Checking your account without spending a request you'll regret
GET /api/v1/credits/balance is the cheapest authenticated call available and a good health check for a stored key â it confirms the key is valid and email_verified without touching quota or starting any work. GET /api/v1/usage gives you your current usage, and GET /api/v1/user returns the account behind the key.
Common gotchas
- A missing language isn't an error. Request
language=deon a video that only has English and you still get200â just withserved_languagedifferent fromrequested_language, and alanguage_hintexplaining how to get German if it's fetchable. - Reading never bills, submitting always does. Every submit â even for a video you already have â charges the full processing price again; only
GETreads,/fetch, and anIdempotency-Keyreplay are free. See how credits work if the billing model is new to you. caption_tracksvs. stored transcripts.caption_trackson a video's metadata is everything YouTube advertises, fetched or not. What's actually stored and readable is a separate, usually shorter, list â checkavailable_languageson the item, notcaption_tracks, before assuming a track is ready to read.
If something fails partway through
A batch can finish partial â some items completed, others didn't. counts in the batch response breaks this down per status (completed, failed, skipped). For the failures worth another attempt, POST /api/v1/transcripts/{batch_id}/retry-failed requeues them (permanent failures like video_unavailable are skipped unless you explicitly ask for include_permanent); see retrying failed items for the full options. A batch you want to abandon entirely can be cancelled with POST /api/v1/transcripts/{batch_id}/cancel â quota and any reserved credits come back.
Frequently asked
Can I submit a channel and some individual videos in one call? Yes â mix any combination of video, playlist, and channel URLs in one urls array. See mixed submissions for exactly how the response is structured when it does.
Do I need to poll if I registered a webhook? No â a webhook (or per-batch callback_url) pushes batch.completed and transcript.ready to you. Polling is the fallback for scripts that can't receive inbound HTTP. See webhooks vs. polling.
What if my request times out and I don't know whether it billed? Retry /resolve with the same Idempotency-Key header instead of guessing â see safe retries with Idempotency-Key.
Where to go next
- Python in ~15 lines â the minimal version of this workflow.
- Webhooks vs. polling â skip the poll loop entirely.
- Idempotency-Key â safe retries on flaky networks.
- Export schema reference â every field name, exactly as returned.
Full interactive reference: API documentation.