Blog / Tutorials / YouTube Transcript API: The Complete Guide
Tutorials 10 min read â€ĸ November 13, 2026

YouTube Transcript API: The Complete Guide

Founder

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:

EndpointBehavior
POST /api/v1/transcriptsAlways async. Returns 202 with a batch_id immediately; you poll or wait for a webhook.
POST /api/v1/transcripts/resolveHybrid. 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 paramWhat it does
include_metadataAdds title, channel, view/like counts, description, tags, etc.
include_segmentsAdds timecoded {text, start, duration} segments (implies metadata).
include_languagesAdds every stored caption track per item, not just the primary one.
languageWhich caption track to serve for this read (falls back to the batch default if missing).
skip / limitPaginate 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 / codeMeaning
402Not enough credits to cover the batch.
429 rate_limitedPer-minute throttle exceeded — honor Retry-After.
409 elevenlabs_disabledA 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=de on a video that only has English and you still get 200 — just with served_language different from requested_language, and a language_hint explaining 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 GET reads, /fetch, and an Idempotency-Key replay are free. See how credits work if the billing model is new to you.
  • caption_tracks vs. stored transcripts. caption_tracks on a video's metadata is everything YouTube advertises, fetched or not. What's actually stored and readable is a separate, usually shorter, list — check available_languages on the item, not caption_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

Full interactive reference: API documentation.

Try Scriptube Free

Extract YouTube transcripts instantly. No credit card required.

Get Started

Related Articles

Tutorials

Python YouTube Transcript API Tutorial

You can fetch a YouTube transcript with Scriptube's API in about 15 lines of Python, using nothing but the requests library. This walks through the short...

Tutorials

Zapier, Make, and Pipedream: Connecting YouTube Transcripts Without a Native App

Scriptube does not have a native Zapier app, Make.com app, or Pipedream integration — say that plainly, because searching for "Scriptube Zapier integration"...

Tutorials

Send YouTube Transcripts to Google Sheets Automatically

Getting a new row in Google Sheets every time Scriptube finishes a transcript takes one webhook and one of two receivers: a Make scenario, or a small Google...