A Fact-Checking Workflow Built on YouTube Transcripts
Zied · 8/7/2026 · 8 min read
A Fact-Checking Workflow Built on YouTube Transcripts
When a politician gives a long interview or a CEO makes claims in a press Q&A, the video goes up on YouTube and the quote-checking starts. The problem is that watching a 90-minute video to verify a single sentence is slow, error-prone, and hard to document. A programmatic transcript gives you a searchable, timestamped record you can cite.
This post walks through a complete workflow: pulling transcripts with timestamps, searching for specific claims, cross-referencing statements across multiple videos, and packaging evidence with source links a colleague or reader can verify independently.
Why Transcripts Beat Manual Review
Manual video review has three compounding problems. First, it does not scale: one journalist can only watch so many hours of footage before a deadline. Second, it produces no artifact: you noted the quote, but where is the proof you checked it against the original? Third, it is non-searchable: if the claim reappears in a different video, you start over from scratch.
A transcript API solves all three. You get a JSON array of text segments, each with a start time in seconds and a duration. That data structure is searchable with a string match, storable in any database, and reproducible by anyone who reads your methodology.
Step 1: Pull the Transcript with Timestamps
Start with a single video. The transcript endpoint accepts any YouTube video URL or ID and returns segments with timing data by default.
curl "https://getyoutubetranscriber.com/api/v2/transcript?video_url=VIDEO_ID&include_timestamp=true&send_metadata=true" \
-H "Authorization: Bearer YOUR_API_KEY"
The response looks like this:
{
"video_id": "abc123xyz",
"language": "en",
"transcript": [
{ "text": "We have never seen numbers like this before.", "start": 412.4, "duration": 3.1 },
{ "text": "The figure stands at forty billion dollars.", "start": 415.5, "duration": 4.0 },
{ "text": "No one disputes that.", "start": 419.5, "duration": 2.2 }
],
"metadata": {
"title": "CEO Q&A: Full Interview",
"author_name": "TechChannel",
"author_url": "https://www.youtube.com/@TechChannel"
}
}
Each segment's start value is seconds from the beginning of the video. To build a deep-link that jumps to that exact moment, append ?t=SECONDS to the video URL:
https://www.youtube.com/watch?v=abc123xyz&t=415
That link is your citation anchor. Anyone who clicks it lands at the exact sentence you quoted.
One thing to keep in mind: if the video has both auto-generated and manually uploaded captions, the default response returns the best available track. If you need a specific language or the manually authored version, pass the lang parameter explicitly (for example, lang=en). See the YouTube Captions API: Auto vs Manual Captions Explained post for how to choose between the two track types when accuracy matters most.
Step 2: Search Within the Transcript
Once you have the JSON, searching for a claim is a string scan. Here is a simple Python function that finds matching segments and returns surrounding context:
import json
def find_claim(transcript_segments, keyword, context_window=2):
matches = []
for i, seg in enumerate(transcript_segments):
if keyword.lower() in seg["text"].lower():
start_idx = max(0, i - context_window)
end_idx = min(len(transcript_segments), i + context_window + 1)
context = transcript_segments[start_idx:end_idx]
matches.append({
"matched_segment": seg,
"context": context,
"timestamp_seconds": seg["start"],
"deep_link": f"https://www.youtube.com/watch?v=VIDEO_ID&t={int(seg['start'])}"
})
return matches
# Load transcript from API response
data = json.loads(api_response_text)
results = find_claim(data["transcript"], "forty billion")
for r in results:
print(f"Found at {r['timestamp_seconds']}s: {r['matched_segment']['text']}")
print(f"Link: {r['deep_link']}")
print("Context:")
for seg in r["context"]:
print(f" [{seg['start']:.1f}s] {seg['text']}")
The context_window parameter pulls in the two segments before and after the match, which is usually enough to confirm a quote is not taken out of context. Adjust it upward for dense technical discussions where the setup spans more sentences.
For JavaScript, the same logic applies with Array.prototype.filter and Array.prototype.findIndex:
function findClaim(segments, keyword, contextWindow = 2) {
const matches = [];
segments.forEach((seg, i) => {
if (seg.text.toLowerCase().includes(keyword.toLowerCase())) {
const start = Math.max(0, i - contextWindow);
const end = Math.min(segments.length, i + contextWindow + 1);
matches.push({
matchedSegment: seg,
context: segments.slice(start, end),
deepLink: `https://www.youtube.com/watch?v=${videoId}&t=${Math.floor(seg.start)}`
});
}
});
return matches;
}
Step 3: Cross-Reference the Same Claim Across Multiple Videos
A single statement in one video is interesting. The same claim repeated across five videos, or contradicted in a later one, is a story. The bulk endpoint handles up to 50 videos in a single POST request:
curl -X POST "https://getyoutubetranscriber.com/api/v2/transcripts-bulk" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_urls": [
"VIDEO_ID_1",
"VIDEO_ID_2",
"VIDEO_ID_3"
],
"include_timestamp": true,
"send_metadata": true
}'
Once you have all the transcripts in memory, run the same find_claim function against each one and collect results tagged by video:
import requests
def bulk_search(video_ids, keyword, api_key):
resp = requests.post(
"https://getyoutubetranscriber.com/api/v2/transcripts-bulk",
headers={"Authorization": f"Bearer {api_key}"},
json={"video_urls": video_ids, "include_timestamp": True, "send_metadata": True}
)
resp.raise_for_status()
results = []
for item in resp.json():
video_id = item["video_id"]
title = item.get("metadata", {}).get("title", video_id)
hits = find_claim(item["transcript"], keyword)
for hit in hits:
hit["video_title"] = title
hit["video_id"] = video_id
results.append(hit)
return results
This gives you a flat list of every occurrence of your keyword across all the videos, each tagged with the source video and a deep-link timestamp. You can sort by timestamp_seconds within each video or group by video_id depending on how you want to present the evidence.
Step 4: Find Relevant Videos to Check
If you are starting from a topic rather than a specific video list, the search endpoint helps you build that list first:
curl "https://getyoutubetranscriber.com/api/v2/search?q=CEO+earnings+call+Q3&type=video" \
-H "Authorization: Bearer YOUR_API_KEY"
The response includes has_captions: true on each result, which tells you before you spend a credit whether the video has a transcript available. Filter on that field to avoid wasting requests on videos without captions.
For monitoring a specific channel over time, the channel videos endpoint returns every upload in reverse chronological order. Pair it with a database that stores which video IDs you have already processed, and you get a lightweight pipeline that checks new uploads automatically. The Track Competitor YouTube Channels via API post covers that monitoring pattern in more detail.
Step 5: Export Evidence Packages
A fact-checking deliverable is only as useful as its documentation. Every claim you flag should ship with a structured record that another journalist or editor can independently verify.
import json
from datetime import datetime
def build_evidence_package(keyword, search_results):
package = {
"claim_searched": keyword,
"generated_at": datetime.utcnow().isoformat() + "Z",
"total_hits": len(search_results),
"evidence": []
}
for r in search_results:
package["evidence"].append({
"video_title": r["video_title"],
"video_id": r["video_id"],
"youtube_url": f"https://www.youtube.com/watch?v={r['video_id']}",
"timestamp_seconds": r["matched_segment"]["start"],
"deep_link": r["deep_link"],
"quoted_text": r["matched_segment"]["text"],
"surrounding_context": [s["text"] for s in r["context"]]
})
return json.dumps(package, indent=2)
Save that JSON file alongside your story draft. If a claim is later disputed, you have a reproducible audit trail: the exact keyword you searched, when you ran it, and the deep-link to every match. The International Fact-Checking Network's Code of Principles specifically calls for transparency of sources and methods, and this kind of structured export satisfies that requirement without adding manual documentation overhead.
Ethical and Accuracy Considerations
Transcripts are not verbatim quotes
YouTube auto-generated captions are produced by speech-to-text models. They handle clear, standard speech well but stumble on proper nouns, strong accents, fast speech, and technical terminology. Before publishing any quote, re-listen to the original clip to confirm the transcript matches the audio. A misrecognized word can change the meaning of a sentence entirely.
Manually uploaded captions are more accurate but are still edited by a human with potential motives. Check whether the uploader is the speaker, the speaker's organization, or an independent transcriber.
Context is not optional
A transcript search that returns the matching sentence is a starting point, not a conclusion. The context_window in the examples above is there for a reason: read what came before and after. Claims routinely include qualifiers, conditionals, or hypotheticals that the keyword alone strips away.
The same principle applies to cross-video comparison. If a speaker made a claim in 2021 and contradicted it in 2024, both timestamps belong in your report. Selectively quoting only the earlier video is misleading even if the quote is accurate.
Attribution and deep-linking
Always link to the source video with a timestamp. Quoting a speaker without a link to the original forces your reader to take your word for it. A deep-link with a ?t= parameter lets anyone verify the quote in seconds, which both protects you and respects your reader.
For work that will feed into AI-assisted summarization or claim extraction downstream, the structured JSON format from this workflow integrates directly with LLM pipelines. The Feed YouTube Transcripts to GPT and Claude post covers that handoff in detail.
Rate limits and credit consumption
If you are running bulk checks against dozens of videos, check your credit balance before starting a large batch. The API charges only on successful transcript calls, not on failed requests. Search and channel-resolve endpoints have their own credit costs listed in the docs. Structuring your workflow to search first, filter on has_captions, and then bulk-fetch only the candidates keeps your credit usage predictable.
Putting It Together
The workflow in full:
- Use the search endpoint to find candidate videos for a topic or claim.
- Filter results to those with
has_captions: true. - Fetch transcripts in bulk with timestamps and metadata.
- Run keyword search across all transcript segments.
- Pull surrounding context for each hit.
- Build an evidence package with quoted text, timestamp, deep-link, and video title.
- Re-listen to every clip before publishing any quote.
This replaces hours of manual scrubbing with a reproducible, auditable pipeline. The transcript JSON becomes the paper trail.
To get started, sign up at getyoutubetranscriber.com for 100 free credits and no credit card required, then test the transcript endpoint against a video you already know well to see how the timestamps map to the spoken word. That sanity check takes five minutes and tells you exactly what to expect before you run it on real evidence.