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_typemust match the content. PlusPlus sniffs the actual format fromcontentand rejects a mismatch (in either direction — declaringtext/plainfor timed VTT/SRT content is rejected too) with400naming the detected type. Omitmime_typeto auto-detect it instead. - One transcript per language. A second transcript for the same language
returns
409—PATCHthe 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/vttandtext/srtare pushed to the player. Use those formats if you want captions to change.