FrameFetch
For agents → Try it free
One requests.post, a transcript back

Python Video Transcript API

Get a transcript — plus metadata, frames, on-screen text, or a grounded answer — from any YouTube, TikTok, Instagram, Reddit, or Pinterest video URL, with a single requests.post. No yt-dlp to install and keep updated, no ffmpeg on your machine, no Whisper model to download and run. The heavy parts happen server-side; your code stays one HTTP call.

Get a free key Read the docs

The whole thing that usually isn't

Getting a transcript "in Python" normally means a stack you have to own: yt-dlp pinned and updated every time a site changes its player, ffmpeg installed, a Whisper model downloaded and a GPU to run it on, plus proxy handling when a platform blocks your datacenter IP. FrameFetch runs all of that and hands you the result. Your dependency list is one line — an HTTP client — and it stays that way as the platforms underneath keep moving.

Install nothing but an HTTP client
pip install requests

That's the only dependency. httpx or the standard-library urllib.request work identically — this is a plain JSON-over-HTTPS API.

Get a transcript from a video URL

POST the video's URL with fields: ["transcript"]. You get back the flat transcript text and timestamped segments — plus a source telling you whether it came from the video's own captions or from Whisper transcribing the audio.

import requests

resp = requests.post(
    "https://framefetch.net/v1/extract",
    headers={"Authorization": "Bearer <your-key>"},
    json={
        "url": "https://www.youtube.com/watch?v=Bj9BD2D3DzA",
        "fields": ["metadata", "transcript"],
    },
    timeout=180,
)
resp.raise_for_status()
data = resp.json()

print(data["metadata"]["title"])
print(data["transcript"]["source"], len(data["transcript"]["segments"]))
for seg in data["transcript"]["segments"][:3]:
    print(f"[{seg['start']:.0f}s] {seg['text']}")
# output — a real, measured run against the live API
Tracing the thoughts of a large language model
captions 66
[0s] you often hear that AI is like a black box ...
[7s] you can put something in and get a response ...
[13s] but you don't really know why it gave that ...

This exact call was run against production while writing this page: the transcript came from captions (66 segments) — no audio download, so it's fast and cheap. On a video with no caption track the same call returns source: "whisper" instead, transcribed from the audio server-side; your Python doesn't change. Read the timing as one observation, not a benchmark.

Captions when they exist, Whisper when they don't

You don't pick the transcription path — FrameFetch does, cheapest-first. If the video carries a usable caption track it's fetched directly: exact, fast, and no per-minute audio charge. Only when there's no caption track does it fall back to Whisper on the audio. The transcript.source field reports which happened ("captions" or "whisper"), so in code you can treat them differently if you want — but you never have to run either one.

FieldShapeWhat it is
transcript.textstringThe full transcript as one flat string
transcript.segmentslistCue objects: { start, end, text } with timestamps in seconds
transcript.sourcestring"captions" (fetched) or "whisper" (transcribed from audio)
transcript.langstring | nullDetected language of the track, when known
From a topic, not a URL: search then ask

When you don't have a URL yet, start from a keyword. POST /v1/search returns candidate YouTube videos; take a result's url and pass it to /v1/extract with a top-level ask — a short answer grounded in the transcript with timestamped quotes, instead of pulling the whole transcript into your own code and searching it.

import requests

API, KEY = "https://framefetch.net", "<your-key>"
h = {"Authorization": f"Bearer {KEY}"}

# 1) find candidates from a topic
hits = requests.post(f"{API}/v1/search", headers=h,
    json={"query": "what is an api", "limit": 3}, timeout=60).json()

# 2) ask the top hit a direct question
answer = requests.post(f"{API}/v1/extract", headers=h,
    json={"url": hits["results"][0]["url"],
          "ask": "What is an API in one sentence?"}, timeout=180).json()

print(answer["ask"]["answer"])
print(answer["ask"]["confidence"], len(answer["ask"]["quotes"]), "quotes")
# output — a real run; the answer carries timestamped quotes and a confidence
An API, or Application Programming Interface, is a connection between apps that ...
high 3 quotes

Two calls: one to turn a topic into URLs, one to turn the best URL into a grounded answer. search is YouTube-only and a flat $0.002 per call; ask is a flat $0.0075 and is never cached, so a repeated question always gets a fresh answer.

Handling the money edge in code

The free tier is 100 calls a month. When credit runs out, the API returns 402 — with a machine-readable body so your code can react rather than crash. Catch it explicitly:

resp = requests.post(f"{API}/v1/extract", headers=h, json=payload, timeout=180)

if resp.status_code == 402:
    body = resp.json()  # { error, balance_micros, price_floor_micros, topup }
    raise RuntimeError(f"out of credit: {body['topup']['pricing']}")

resp.raise_for_status()
data = resp.json()

Every response also carries a cost.totalMicros so you can meter spend per call. Prices: metadata $0.00015, transcript $0.0015/min, frames $0.00012 each, on-screen text (OCR) $0.000225 each, ask $0.0075/call, with a $0.002 minimum per call. Full breakdown on pricing.

Get started

Grab a free key with one call — no card, no dashboard step:

import requests
key = requests.post("https://framefetch.net/v1/keys",
    json={"email": "you@example.com"}).json()["key"]
print(key)  # ff_... — 100 free calls a month, use it as the Bearer token above

Get a free key YouTube transcript API n8n workflow