Skip to content

Managing transcripts

A transcript is the text track for a video, exposed as a per-video collection at /videos/{video_id}/transcripts/. Each transcript has a language and a format — WebVTT (text/vtt), SubRip (text/srt), or plain text (text/plain) — and a video has at most one transcript per language.

The common workflow is a round trip: download a transcript, edit the text, and send it back. For natively hosted videos, editing a WebVTT or SubRip transcript also republishes the video's captions on the player — so correcting a typo in the transcript fixes it for viewers too.

Formats and the Mux caption sync

Format mime_type Pushed to video captions?
WebVTT text/vtt ✅ Yes (native videos)
SubRip text/srt ✅ Yes (native videos)
Plain text text/plain ❌ Stored only

For native videos, creating or editing a text/vtt/text/srt transcript replaces the caption track on the underlying video. That sync is asynchronous — the API returns immediately and the new captions appear on the player a few moments later. Plain-text transcripts are stored for download and search but never change the player captions. Linked (YouTube/Vimeo) videos store transcripts but have no captions to sync.

Download → edit → re-upload

1. Find the transcript

curl https://acme.plusplus.app/api/v2/videos/$VIDEO_ID/transcripts/ \
  -H "Authorization: Bearer pp_your_token_here"
{
  "data": [
    {
      "public_id": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
      "name": "English (CC)",
      "language_code": "en",
      "mime_type": "text/vtt",
      "size": 2048,
      "summary": "A walkthrough of Q2 results and the roadmap.",
      "created": "2026-06-01T10:00:00Z",
      "modified": "2026-06-01T10:00:00Z"
    }
  ],
  "meta": { "page": 1, "page_size": 25, "total_count": 1 }
}

The list omits the transcript body. Fetch a single transcript to get its content:

curl https://acme.plusplus.app/api/v2/videos/$VIDEO_ID/transcripts/$TRANSCRIPT_ID/ \
  -H "Authorization: Bearer pp_your_token_here"
{
  "public_id": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
  "language_code": "en",
  "mime_type": "text/vtt",
  "content": "WEBVTT\n\n00:00:00.000 --> 00:00:04.000\nWelcome to the Q2 all-hands.\n"
}

2. Edit and send it back

PATCH the edited content. For a native video this republishes the captions.

curl -X PATCH https://acme.plusplus.app/api/v2/videos/$VIDEO_ID/transcripts/$TRANSCRIPT_ID/ \
  -H "Authorization: Bearer pp_your_token_here" \
  -H "Content-Type: application/json" \
  -d '{ "content": "WEBVTT\n\n00:00:00.000 --> 00:00:04.000\nWelcome to the Q2 all-hands meeting.\n" }'

To avoid clobbering a concurrent edit, pass the ETag you got from the read as an If-Match header — the update is rejected with 412 if the transcript changed in the meantime.

Creating a new transcript

Add a transcript inline. Provide content, and optionally mime_type, name, and language_code (defaults to en). Omit mime_type to auto-detect the format from content; if you provide it, it must exactly match the content's actual (sniffed) format — a mismatch returns 400 naming the detected type:

curl -X POST https://acme.plusplus.app/api/v2/videos/$VIDEO_ID/transcripts/ \
  -H "Authorization: Bearer pp_your_token_here" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "WEBVTT\n\n00:00:00.000 --> 00:00:04.000\nBienvenidos a la reunión.\n",
    "mime_type": "text/vtt",
    "language_code": "es",
    "name": "Spanish"
  }'

A video may have only one transcript per language. Creating a second for a language that already exists returns 409 — edit the existing one with PATCH instead.

Uploading large transcript files

For large files, use the presigned upload flow instead of inlining the text: request a URL, PUT the file straight to storage, then poll until it's ingested.

sequenceDiagram
    participant Your app
    participant PlusPlus API
    participant Storage
    Your app->>PlusPlus API: POST …/transcripts/uploads/ { mime_type, language_code }
    PlusPlus API-->>Your app: 201 { upload_id, upload_url, transcript_id }
    Your app->>Storage: PUT (the .vtt/.srt file)
    Storage-->>Your app: 200
    loop until ready or errored
        Your app->>PlusPlus API: GET …/transcripts/uploads/{upload_id}/status/
        PlusPlus API-->>Your app: 200 { status }
    end
curl -X POST https://acme.plusplus.app/api/v2/videos/$VIDEO_ID/transcripts/uploads/ \
  -H "Authorization: Bearer pp_your_token_here" \
  -H "Content-Type: application/json" \
  -d '{ "mime_type": "text/vtt", "language_code": "en" }'
{
  "upload_id": "7f3c1a9b2d4e4f6a8b0c1d2e3f405162",
  "upload_url": "https://s3.amazonaws.com/plusplus-uploads/…",
  "upload_type": "s3_presigned_put",
  "transcript_id": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
  "video_id": "c8a7f1e2-4a3b-4e5f-8c9d-0123456789ab",
  "expires_at": "2026-06-01T11:00:00Z",
  "status": "awaiting_upload"
}

PUT the file to upload_url with a matching Content-Type, then poll status:

curl https://acme.plusplus.app/api/v2/videos/$VIDEO_ID/transcripts/uploads/$UPLOAD_ID/status/ \
  -H "Authorization: Bearer pp_your_token_here"
status Meaning
awaiting_upload URL issued; the file hasn't landed yet.
ready Content ingested. For native videos the caption sync has been queued.
errored The uploaded file was unreadable or failed format validation.

Polling is what triggers ingestion — poll every 3–5 seconds. There's no separate confirm call.

Removing a transcript

curl -X DELETE https://acme.plusplus.app/api/v2/videos/$VIDEO_ID/transcripts/$TRANSCRIPT_ID/ \
  -H "Authorization: Bearer pp_your_token_here"

This removes the transcript from PlusPlus. Any caption track already on the Mux video is left intact — deleting a transcript never strips captions from the player.

Common pitfalls

  • A declared mime_type must match the content. PlusPlus sniffs the actual format from content and rejects a mismatch (in either direction — declaring text/plain for timed VTT/SRT content is rejected too) with 400 naming the detected type. Omit mime_type to auto-detect it instead.
  • One transcript per language. A second transcript for the same language returns 409PATCH the existing one instead.
  • Captions don't change instantly. The Mux sync runs in the background after the response; allow a few moments for new captions to appear.
  • Plain text won't update captions. Only text/vtt and text/srt are pushed to the player. Use those formats if you want captions to change.