New: try each tool in your browser, no account needed. Then one API key with 3 free tries of every API. Get your key →
Video5 min read

How to get a transcript, summary and subtitles from a YouTube video with an API

Turn a public YouTube link or your own video file into a timestamped transcript, a summary, a description of what is on screen and SRT or VTT subtitles, with one API request. Working curl, Python and Node.js code.

OpenCorpo Team

You have a YouTube link and you want what's in it as data: the transcript with timestamps, a short summary, maybe subtitles. The usual ways of getting that from code tend to break:

  • The official YouTube Data API lets you download caption tracks only with the authorization of the channel that owns the video. For anyone else's video it does not help.
  • Scraping the caption endpoints or running a downloader works on your laptop and then fails in production. YouTube blocks most cloud and datacenter IP ranges and increasingly asks for sign-in or proof-of-origin tokens.
  • Captions are not the video. Many videos have no captions, auto-captions mangle names and jargon, and a transcript misses everything shown but not said: slides, code on screen, product demos.

This guide takes a different route. You send the link, and a multimodal model watches and listens to the video in one pass. You get back a timestamped transcript, a summary, a timestamped description of what is on screen, and subtitle files.

How it works

The Video API hands the YouTube URL to its model provider, which fetches the video on its own side. OpenCorpo never downloads YouTube videos. The model processes audio and frames together and returns four possible outputs:

Output What you get
summary One paragraph about the whole video
transcript Speech, as timestamped segments with speaker labels
description What is on screen, as timestamped segments
prompt A prompt to recreate a similar video with an AI generator (separate guide)

You can request any combination. The price is the same: a video is billed once per started minute, whatever you ask for.

Step 1: Create an API key

Create an account at app.opencorpo.com and create a key under API keys. Your first 3 video analyses are free, each covering up to the first 5 minutes of the video.

bash
export OPENCORPO_API_KEY=sk_live_...

Step 2: Start an analysis

For a short video, the simplest call holds the request open until the job finishes (up to 90 seconds) with "wait": true:

bash
curl -X POST https://api.opencorpo.com/v1/video/analyze \
  -H "X-API-Key: $OPENCORPO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7c1e9f40-talk-summary" \
  -d '{
    "source": {"type": "youtube", "url": "https://www.youtube.com/watch?v=jNQXAC9IVRw"},
    "outputs": ["summary", "transcript"],
    "wait": true
  }'

The Idempotency-Key header makes retries safe: sending the same key again returns the same job instead of creating and billing a second one.

You get 200 with the finished job, or 202 if it is still running:

json
{
  "id": "vj_4e7ce795b4a68215a7ac69db",
  "status": "completed",
  "source": { "type": "youtube", "url": "https://www.youtube.com/watch?v=jNQXAC9IVRw" },
  "requested_outputs": ["summary", "transcript"],
  "model": "balanced",
  "duration": "00:19",
  "truncated": false,
  "billed_minutes": 1,
  "charged_eur": "0.050000",
  "results": {
    "summary": "A young man stands in front of an elephant enclosure at a zoo ...",
    "transcript": [
      { "start": "00:01", "end": "00:04", "start_seconds": 1.0, "end_seconds": 4.0,
        "speaker": "Speaker 1", "text": "Alright, so here we are in front of the, uh, elephants." }
    ],
    "transcript_language": "en"
  }
}

Each segment has both a display timestamp ("00:01") and seconds (1.0), so you can link straight into the video with &t=1s or align the text with your own player.

Step 3: Wait for the job

Anything longer than a short clip takes more than 90 seconds. Start the job without wait, then long-poll GET /v1/video/jobs/{id}?wait=60, which returns as soon as the job finishes or after 60 seconds. No sleeping or tight polling loops needed.

Python

python
import os
import uuid
import requests

API = "https://api.opencorpo.com"
HEADERS = {"X-API-Key": os.environ["OPENCORPO_API_KEY"]}


def check(r):
    if not r.ok:
        err = r.json()["error"]
        raise RuntimeError(f"{err['code']}: {err['message']}")
    return r.json()


def analyze_youtube(url, outputs=("summary", "transcript"), idempotency_key=None):
    # Reuse the same key if you retry this exact request after a network error.
    key = idempotency_key or str(uuid.uuid4())
    job = check(requests.post(
        f"{API}/v1/video/analyze",
        headers={**HEADERS, "Idempotency-Key": key},
        json={"source": {"type": "youtube", "url": url}, "outputs": list(outputs)},
        timeout=30,
    ))
    while job["status"] in ("queued", "processing"):
        job = check(requests.get(
            f"{API}/v1/video/jobs/{job['id']}",
            headers=HEADERS, params={"wait": 60}, timeout=100,
        ))
    if job["status"] != "completed":
        raise RuntimeError(f"job {job['id']} {job['status']}: {job.get('error')}")
    return job


job = analyze_youtube("https://www.youtube.com/watch?v=jNQXAC9IVRw")
print(job["results"]["summary"])
for seg in job["results"]["transcript"]:
    print(f"[{seg['start']}] {seg['speaker']}: {seg['text']}")

Node.js (20+)

javascript
const API = "https://api.opencorpo.com";
const headers = { "X-API-Key": process.env.OPENCORPO_API_KEY };

async function check(res) {
  const body = await res.json();
  if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
  return body;
}

export async function analyzeYoutube(url, outputs = ["summary", "transcript"]) {
  let job = await check(await fetch(`${API}/v1/video/analyze`, {
    method: "POST",
    headers: { ...headers, "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID() },
    body: JSON.stringify({ source: { type: "youtube", url }, outputs }),
  }));
  while (job.status === "queued" || job.status === "processing") {
    job = await check(await fetch(`${API}/v1/video/jobs/${job.id}?wait=60`, { headers }));
  }
  if (job.status !== "completed") throw new Error(`job ${job.id} ${job.status}`);
  return job;
}

const job = await analyzeYoutube("https://www.youtube.com/watch?v=jNQXAC9IVRw");
console.log(job.results.summary);

A failed job carries an error object instead of results, for example youtube_private or youtube_unavailable, and is not charged.

Step 4: Export SRT or VTT subtitles

Once a job is completed, export its transcript as a subtitle file:

bash
curl -o talk.srt \
  "https://api.opencorpo.com/v1/video/jobs/vj_4e7ce795b4a68215a7ac69db/export?format=srt&source=transcript" \
  -H "X-API-Key: $OPENCORPO_API_KEY"

format can be srt, vtt, txt or json. Set source=description to export the on-screen description with the same timing. As a VTT track, that can be a starting point for an audio-description or "what's on screen" track, to review before use.

Analysing your own video files

The same endpoint works for files you own. Get a presigned upload URL, PUT the bytes straight to storage, then pass the upload_id. Files up to 500 MB go through this route and are deleted automatically after 7 days.

bash
# 1. Ask for an upload URL
curl -X POST https://api.opencorpo.com/v1/uploads \
  -H "X-API-Key: $OPENCORPO_API_KEY" -H "Content-Type: application/json" \
  -d '{"filename": "keynote.mp4", "content_type": "video/mp4", "size_bytes": 84213555}'

# 2. Upload the file to the returned URL
curl -X PUT "$UPLOAD_URL" -H "Content-Type: video/mp4" --data-binary @keynote.mp4

# 3. Analyse it
curl -X POST https://api.opencorpo.com/v1/video/analyze \
  -H "X-API-Key: $OPENCORPO_API_KEY" -H "Content-Type: application/json" \
  -d '{"source": {"type": "upload", "upload_id": "up_..."}, "outputs": ["summary", "transcript", "description"]}'

Options worth knowing

Option Values When to use it
options.model balanced (default), fast, quality quality costs 3× and helps with hard audio or dense scenes
options.detail standard (default), high high costs 2×; use it for slides, screen recordings and text-heavy video
options.language e.g. fr, de A hint for the spoken language; default is detection
options.fps above 0, at most 2 Frames analysed per second; the default is chosen for the outputs you request

What it costs

Analysis is billed per started minute of analysed video, once per video: €0.05 / minute on the default model. A 12-minute talk is billed as 12 minutes whether you ask for one output or all four. Before a job starts, the cost of the longest analysis your balance allows is reserved, and only the actual length is charged. If your balance covers less than the whole video, the analysis stops there and the job is marked truncated. Live prices are on the pricing page.

Limits to plan for

  • Public YouTube videos only. Private and sign-in-only videos fail with youtube_private; removed or region-locked ones with youtube_unavailable. Upload the file instead if you own it.
  • 20 minutes per video. Longer videos are analysed up to 20 minutes and marked truncated.
  • 3 video jobs at a time per account; further jobs get 429 concurrency_limit.
  • No translation. Transcripts are written in the language spoken.
  • Speaker labels are generic ("Speaker 1", "Speaker 2"), not names.
  • Second-level timestamps. Fine for navigation and subtitles, not for frame-accurate editing.
  • Model output. Names, numbers and jargon can be misheard. Review before you publish a transcript or quote from it.
  • Your responsibility for the content. Make sure you have the right to process and reuse the videos you analyse.

Full request and response reference: Video API docs.

Frequently asked questions

Does it work for YouTube videos without captions?

Yes. The model listens to the audio and watches the frames itself; it does not read YouTube's caption tracks, so videos without captions work the same way.

Can I get SRT or VTT subtitles?

Yes. Once a job is completed, GET /v1/video/jobs/{id}/export?format=srt (or vtt, txt, json) returns the transcript as a subtitle file. The timestamped description can be exported the same way.

Which languages are supported?

The transcript is written in the language spoken in the video. You can pass a hint such as options.language = fr; transcripts are never translated.

What happens with videos longer than 20 minutes?

Up to 20 minutes are analysed per video. For a longer video, the first 20 minutes are analysed and the job is marked truncated.

Do private or unlisted videos work?

Use public videos. A private or sign-in-only video fails with youtube_private, and a removed or region-locked one with youtube_unavailable. Failed jobs are not charged. For your own private footage, upload the file instead.

Do I pay more for more outputs?

No. A video is billed once per started minute (€0.05 / minute on the default model), whatever combination of summary, description, transcript and prompt you request.