← Back to blog

Turn YouTube Videos Into Blog Posts, Newsletters, and Social Threads with the Transcript API

Every team produces more video than text: demos, conference talks, onboarding walkthroughs, podcast episodes, customer interviews. All of it is knowledge trapped in an async format that search engines barely index and readers rarely finish. Yet the text version of that same content — a blog post, a newsletter section, a social thread — is what actually gets shared, cited, and ranked.

The good news: converting a video into written content is no longer a manual transcription chore. With the FetchAPI YouTube Transcript API, you can pull a clean, timestamped transcript with one GET request and feed it straight into an LLM. In this tutorial you'll build a content repurposing pipeline that turns any YouTube video into three ready-to-edit assets — a long-form blog post, a newsletter blurb, and a social thread — while keeping accurate links back to the exact moment in the video.

Why repurpose video instead of just embedding it

The pipeline uses two API calls

Step Endpoint What you get
Metadata GET /v1/info Title, channel, thumbnail, views, upload date, duration, has_transcript
Content GET /v1/transcript Timestamped caption segments

Both are plain GETs with a url parameter and return compact JSON, so there's no HTML to parse and no scraping infrastructure to maintain.

Step 1 — Grab the video metadata

Metadata becomes your SEO front-matter: the title seeds the headline, the thumbnail becomes the hero image, and the upload date plus channel give you attribution.

curl -s "https://fetchapi.tech/v1/info?url=https://www.youtube.com/watch?v=dQw4w9WgXcQ"
{
  "video_id": "dQw4w9WgXcQ",
  "title": "Rick Astley - Never Gonna Give You Up (Official Video) (4K Remaster)",
  "channel": "Rick Astley",
  "channel_url": "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw",
  "duration_seconds": 213,
  "duration_formatted": "3:33",
  "view_count": 1825480624,
  "upload_date": "20091025",
  "thumbnail": "https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg",
  "has_transcript": true
}

The has_transcript flag is the cheapest filter you have: check it before queuing a video for repurposing, so you don't waste work on caption-less uploads.

Step 2 — Grab the transcript

curl -s "https://fetchapi.tech/v1/transcript?url=https://www.youtube.com/watch?v=dQw4w9WgXcQ" \
  | jq '.transcript[0:3]'
[
  { "start": 21.79, "end": 21.8,  "text": "We're no strangers to",                                        "start_formatted": "0:21", "end_formatted": "0:21" },
  { "start": 21.8,  "end": 25.95, "text": "We're no strangers to love. You know the rules and so do",     "start_formatted": "0:21", "end_formatted": "0:25" },
  { "start": 25.95, "end": 25.96, "text": "love. You know the rules and so do",                           "start_formatted": "0:25", "end_formatted": "0:25" }
]

Each segment carries start, end, text, and pre-formatted timestamps. The start seconds are gold — they let you deep-link a citation to the exact moment in the video.

Step 3 — Clean the rolling captions

Look closely at those three segments. They overlap: line two repeats line one and adds a few words. That's how YouTube's auto-captions work — they roll, re-emitting the previous line with new text appended. Feed that raw into an LLM and you get duplicated sentences and a bloated prompt.

The fix is to collapse consecutive segments where one is a prefix of the next:

def clean_transcript(segments):
    """Collapse YouTube's rolling auto-caption overlaps into one row per beat."""
    lines = []
    for seg in segments:
        text = seg["text"].strip()
        if not text:
            continue
        if lines and text.startswith(lines[-1]["text"]):
            # longer rolling caption — replace the shorter one, keep the earlier start
            lines[-1]["text"] = text
        elif lines and lines[-1]["text"].startswith(text):
            continue  # pure duplicate of a line we already have
        else:
            lines.append({
                "text": text,
                "start": seg["start"],
                "start_formatted": seg.get("start_formatted", ""),
            })
    return lines

After cleaning, those three overlapping captions become a single line: "We're no strangers to love. You know the rules and so do…". Your token count drops, and the model no longer sees phantom repetition.

Step 4 — Chunk to a token budget

A 60-minute video is roughly 9,000–11,000 words — far too much for a single prompt. Merge cleaned lines into ~700-word chunks, keeping the start timestamp of each chunk so you can cite it later:

def chunk(lines, max_words=700):
    chunks, current, count, start = [], [], 0, None
    for line in lines:
        if start is None:
            start = line["start_formatted"]
        words = line["text"].split()
        current.append(line["text"])
        count += len(words)
        if count >= max_words:
            chunks.append({"text": " ".join(current), "start": start})
            current, count, start = [], 0, None
    if current:
        chunks.append({"text": " ".join(current), "start": start})
    return chunks

Step 5 — Ask the model for three formats at once

One transcript, three outputs. A single structured prompt gets you all of them in one pass:

import json, requests
from openai import OpenAI

client = OpenAI()

SYSTEM = """You are an editor who turns video transcripts into written content.
Return strict JSON with three keys:
- blog: a 700-900 word markdown article with an H2 structure
- newsletter: a 90-word teaser ending with a call to watch the video
- thread: a list of 6-8 tweets, each under 280 characters
Use only facts present in the transcript. Never invent quotes or stats."""

def repurpose(video_url):
    meta = requests.get("https://fetchapi.tech/v1/info",
                        params={"url": video_url}, timeout=20).json()
    segs = requests.get("https://fetchapi.tech/v1/transcript",
                        params={"url": video_url}, timeout=30).json()["transcript"]
    transcript = " ".join(l["text"] for l in clean_transcript(segs))

    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        response_format={"type": "json_object"},
        messages=[
            {"role": "system", "content": SYSTEM},
            {"role": "user", "content":
                f"Video: {meta['title']} by {meta['channel']}\n\n"
                f"Transcript:\n{transcript[:24000]}"},
        ],
    )
    return meta, json.loads(resp.choices[0].message.content)

meta, assets = repurpose("https://www.youtube.com/watch?v=dQw4w9WgXcQ")
print(assets["blog"][:400])

Because you asked for json_object, you get three clean fields you can route to your CMS, email tool, and scheduler without a parsing layer.

Step 6 — Attach timestamp deep links

The thing that makes repurposed content better than a plain summary is traceability. When the model quotes a line, link it back:

def deep_link(video_id, start_seconds):
    return f"https://youtu.be/{video_id}?t={int(start_seconds)}"

So a paraphrase from a segment that starts at 4:12 becomes a link that drops the reader exactly there. Readers trust sourced content, and every one of those links is another signal that the video and the post belong together.

Production tips

Wrap-up

Video is your highest-effort content and your least-indexed. The Transcript API closes that gap: two GET requests turn a video into SEO-ready metadata and clean, timestamped text, and a single LLM pass turns that text into a blog post, a newsletter, and a thread.

Try it on your own channel right now:

curl -s "https://fetchapi.tech/v1/transcript?url=https://www.youtube.com/watch?v=dQw4w9WgXcQ" \
  | jq -r '.transcript[0:5][].text'

Then browse the full API docs and start shipping more text from the video you already made.