← Back to blog

Building an Accessibility Captioning Workflow

David Boulen · 7/13/2026 · 9 min read

Building an Accessibility Captioning Workflow

Building an Accessibility Captioning Workflow

Adding captions to video content used to mean uploading a file to YouTube and hoping the auto-generated track was close enough. That approach no longer holds up legally or technically. The EU Accessibility Act began enforcement on June 28, 2025, requiring WCAG 2.1 AA compliance across digital products. Digital accessibility lawsuits in the United States rose 37% in 2025 year-over-year. Courts and regulators increasingly treat inaccurate captions the same way they treat no captions at all.

This guide walks through a production-grade captioning pipeline using the YouTube Transcriber API: pulling timed transcripts, converting them to SRT and VTT files, handling multiple languages, doing a targeted human review pass, and delivering captions to a video player. Every step uses real API calls with copy-paste code.

Developer writing code for an accessible captioning workflow

Why Accurate Captions Matter for Compliance

WCAG 2.1 Success Criterion 1.2.2 (Level AA) requires captions for all pre-recorded video with audio. The requirement is not just presence - it is accuracy. A caption track that misreads proper nouns, drops punctuation, or omits speaker labels fails the criterion the same as no captions at all.

Auto-generated YouTube captions are convenient for discovery but they consistently fall short of what auditors flag as compliant. They lack sentence-ending punctuation, misread technical terms and brand names, and do not identify speakers. Running a human review pass before publishing is not optional if compliance is the goal.

Beyond the legal angle, the business case for accurate captions is strong on its own. Around 80% of people who use captions are not deaf or hard of hearing - they use them in noisy environments, while learning a language, or when they cannot play audio. About 85% of social media video is watched with sound off. Captioned videos see up to 40% longer average watch duration and 26% higher click-through rates on CTAs, according to research compiled by 3Play Media. Captions are audience expansion, not just accommodation.

The regulatory landscape breaks down like this:

| Regulation | Status | Standard Required | | - -| - -| - -| | EU Accessibility Act (EAA) | Enforcing since June 28, 2025 | WCAG 2.1 AA | | ADA Title II (entities serving 50k+ population) | Deadline April 26, 2027 | WCAG 2.1 AA | | ADA Title II (smaller entities) | Deadline April 26, 2028 | WCAG 2.1 AA | | ADA Title III (private sector) | Ongoing enforcement | Courts apply WCAG 2.1 AA |

If your video content is public-facing, one or more of these applies to you today.

Pulling Transcripts and Aligning Timing

The foundation of any captioning workflow is accurate, timed transcript data. The YouTube Transcriber API's transcript endpoint returns exactly this: an array of segments, each with the spoken text, a start time in seconds, and a duration in seconds.

curl "https://getyoutubetranscriber.com/api/v2/transcript?video_url=dQw4w9WgXcQ&include_timestamp=true" \
  -H "Authorization: Bearer YOUR_API_KEY"

The response looks like this:

{
  "video_id": "dQw4w9WgXcQ",
  "language": "en",
  "transcript": [
    {
      "text": "We're no strangers to love",
      "start": 18.24,
      "duration": 2.16
    },
    {
      "text": "You know the rules and so do I",
      "start": 20.4,
      "duration": 2.64
    }
  ]
}

Each segment gives you the three values you need to build a subtitle file: the text, where it starts, and how long it shows. The include_timestamp parameter defaults to true, so you get timing data unless you explicitly disable it.

The start values are floating-point seconds from the beginning of the video. To convert them to the HH:MM:SS,mmm format that SRT and VTT require, you split the decimal into whole seconds and milliseconds. Here is a Python helper that does the conversion and formats both start and end timestamps:

def seconds_to_srt_time(seconds: float) -> str:
    ms = int((seconds % 1) * 1000)
    s = int(seconds) % 60
    m = (int(seconds) // 60) % 60
    h = int(seconds) // 3600
    return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}"

def seconds_to_vtt_time(seconds: float) -> str:
    # VTT uses a period instead of a comma
    return seconds_to_srt_time(seconds).replace(",", ".")

Generating Multi-Language Subtitle Files

Once you have the timing-aligned transcript, generating SRT and VTT files is a straightforward serialization step. The harder problem at scale is handling multiple languages without multiplying manual work.

The lang parameter on the transcript endpoint requests a caption track in any of the 125+ languages the video supports. For a video that has French, Spanish, and German caption tracks, you can pull all three with a simple loop. If you are working with videos that only have English captions, you would run a translation step downstream - but for videos with native multilingual tracks, the API handles it directly.

For more on the language coverage and how language codes work, see the guide Get YouTube Transcripts in 125+ Languages.

Python: Generate SRT and VTT for Multiple Languages

import requests

API_KEY = "YOUR_API_KEY"
VIDEO_ID = "dQw4w9WgXcQ"
LANGUAGES = ["en", "fr", "es", "de"]

def seconds_to_srt_time(seconds: float) -> str:
    ms = int((seconds % 1) * 1000)
    s = int(seconds) % 60
    m = (int(seconds) // 60) % 60
    h = int(seconds) // 3600
    return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}"

def segments_to_srt(segments: list) -> str:
    lines = []
    for i, seg in enumerate(segments, start=1):
        start = seconds_to_srt_time(seg["start"])
        end = seconds_to_srt_time(seg["start"] + seg["duration"])
        lines.append(f"{i}\n{start} --> {end}\n{seg['text']}\n")
    return "\n".join(lines)

def segments_to_vtt(segments: list) -> str:
    lines = ["WEBVTT", ""]
    for seg in segments:
        start = seconds_to_srt_time(seg["start"]).replace(",", ".")
        end = seconds_to_srt_time(seg["start"] + seg["duration"]).replace(",", ".")
        lines.append(f"{start} --> {end}\n{seg['text']}\n")
    return "\n".join(lines)

for lang in LANGUAGES:
    resp = requests.get(
        "https://getyoutubetranscriber.com/api/v2/transcript",
        params={"video_url": VIDEO_ID, "lang": lang, "include_timestamp": "true"},
        headers={"Authorization": f"Bearer {API_KEY}"},
    )
    resp.raise_for_status()
    data = resp.json()
    segments = data["transcript"]

    with open(f"{VIDEO_ID}_{lang}.srt", "w", encoding="utf-8") as f:
        f.write(segments_to_srt(segments))

    with open(f"{VIDEO_ID}_{lang}.vtt", "w", encoding="utf-8") as f:
        f.write(segments_to_vtt(segments))

    print(f"Generated captions for lang={lang}: {len(segments)} segments")

This produces one SRT and one VTT file per language. For a playlist or full channel, wrap the outer loop over video IDs and use the bulk transcript endpoint (up to 50 videos per call) to avoid making individual requests for each video.

Node.js: SRT Generation

const fetch = require("node-fetch");
const fs = require("fs");

const API_KEY = "YOUR_API_KEY";
const VIDEO_ID = "dQw4w9WgXcQ";

function toSrtTime(seconds) {
  const ms = Math.round((seconds % 1) * 1000);
  const s = Math.floor(seconds) % 60;
  const m = Math.floor(seconds / 60) % 60;
  const h = Math.floor(seconds / 3600);
  return `${String(h).padStart(2,"0")}:${String(m).padStart(2,"0")}:${String(s).padStart(2,"0")},${String(ms).padStart(3,"0")}`;
}

async function fetchAndGenerateSrt(lang) {
  const url = new URL("https://getyoutubetranscriber.com/api/v2/transcript");
  url.searchParams.set("video_url", VIDEO_ID);
  url.searchParams.set("lang", lang);
  url.searchParams.set("include_timestamp", "true");

  const res = await fetch(url.toString(), {
    headers: { Authorization: `Bearer ${API_KEY}` },
  });

  if (!res.ok) throw new Error(`API error ${res.status}`);
  const { transcript } = await res.json();

  const srt = transcript
    .map((seg, i) => {
      const start = toSrtTime(seg.start);
      const end = toSrtTime(seg.start + seg.duration);
      return `${i + 1}\n${start} --> ${end}\n${seg.text}\n`;
    })
    .join("\n");

  fs.writeFileSync(`${VIDEO_ID}_${lang}.srt`, srt, "utf8");
  console.log(`Wrote ${VIDEO_ID}_${lang}.srt (${transcript.length} segments)`);
}

fetchAndGenerateSrt("en").catch(console.error);

Person working on video captioning and accessibility features at a laptop

Reviewing and Editing Auto-Generated Text

The SRT and VTT files you generate at this point are based on YouTube's caption tracks, which are often auto-generated. Before publishing, run a review pass targeting the specific failure modes that auditors flag:

Punctuation gaps. Auto-generated captions frequently omit periods, commas, and question marks. Screen readers and human readers both rely on sentence-ending punctuation to parse meaning. Add it.

Proper nouns and technical terms. Brand names, product names, and technical vocabulary are the most common transcription errors. Search the SRT file for terms you know should appear and correct them. A quick grep for known brand names in the transcript will surface most of these.

Speaker identification. If the video has multiple speakers, WCAG guidance recommends labeling them in the caption text (e.g., "[HOST]:" or "[GUEST]:"). Auto-generated tracks do not do this.

Sound descriptions. For content where non-speech audio is meaningful - applause, music cues, alarms - you should add descriptive text in brackets: [applause], [upbeat music]. This matters most for content where context depends on the audio environment.

A practical review workflow: load the SRT into a subtitle editor (tools like Aegisub or Subtitle Edit are free and open source), play the video with the caption track active, and correct errors as you encounter them. For long-form content at volume, you can automate a first pass that flags low-confidence segments by looking for missing terminal punctuation or segments shorter than 1.5 seconds, which are common signs of garbled auto-caption output.

Callout: WCAG 2.1 Success Criterion 1.2.2 does not specify an accuracy percentage threshold in its text, but accessibility auditors and courts have treated 98%+ accuracy as the practical standard. Anything below 95% accuracy is a clear compliance risk.

Delivering Captions to Your Video Player

Once your SRT and VTT files pass review, the delivery step depends on where the video is hosted.

HTML5 Video with Track Element

VTT is the native format for the HTML5 <track> element. Serve your VTT files from the same origin as the page (or with correct CORS headers if cross-origin) and reference them as track elements:

<video controls>
  <source src="video.mp4" type="video/mp4" />
  <track
    kind="captions"
    src="captions_en.vtt"
    srclang="en"
    label="English"
    default
  />
  <track
    kind="captions"
    src="captions_fr.vtt"
    srclang="fr"
    label="French"
  />
  <track
    kind="captions"
    src="captions_es.vtt"
    srclang="es"
    label="Spanish"
  />
</video>

Use kind="captions" rather than kind="subtitles". Captions include sound descriptions for deaf and hard-of-hearing viewers; subtitles assume the viewer can hear the audio and only translates speech. WCAG requires captions specifically.

Uploading to YouTube

If the video lives on YouTube, the YouTube Data API v3 captions.insert method accepts SRT files. You can also upload directly via the YouTube Studio interface. Either way, upload language-specific tracks separately with the correct language code. YouTube will display the user's language preference automatically if a matching track exists.

Video Hosting Platforms (Vimeo, Wistia, JW Player)

Most hosted video platforms accept SRT via their dashboard or API. Check whether the platform requires UTF-8 encoding (all do) and whether they accept VTT natively or convert from SRT on upload. Wistia and JW Player both accept SRT and VTT. Vimeo accepts SRT.

For a deeper walkthrough of SRT and VTT file generation including format edge cases, see Generate SRT and VTT Subtitles from YouTube Videos.

Handling Scale: Bulk Caption Generation for a Channel

For a library of existing videos - a course platform, a media archive, a product video catalog - you need to process videos in bulk rather than one at a time.

The pattern is:

  1. Use the channel-videos endpoint to list all video IDs for the channel.
  2. Send batches of up to 50 IDs to the bulk transcript endpoint (/api/v2/transcripts-bulk).
  3. For each video and language combination, run the SRT/VTT conversion and write the output files.
  4. Queue the generated files into your review pipeline before publishing.

The W3C's Web Content Accessibility Guidelines also provide a useful reference point here: Understanding WCAG Success Criterion 1.2.2 explains the full intent and acceptable caption formats in detail, and it is worth reading before you design your review step.

When processing hundreds or thousands of videos, rate limits become relevant. The API handles retries, proxy rotation, and YouTube's anti-bot measures for you, so your client does not need to implement those. For notes on handling the rate limits that apply on your API plan, see Understanding YouTube Transcript Rate Limits.

Where to Go Next

The YouTube Transcriber API's localization-friendly endpoints give you the pieces you need for a complete captioning workflow: timed JSON transcripts, 125+ language support, channel and playlist enumeration, and bulk processing. The engineering surface is small - pull transcripts, convert timing to SRT/VTT, review, deliver.

Start with 100 free credits and no credit card required at getyoutubetranscriber.com/docs. The docs include interactive examples for the transcript, channel, and bulk endpoints so you can test your video IDs before writing any code.

Compliance deadlines are fixed. The pipeline to meet them is not complicated. The main cost is starting late.