PodcastsToText
Transcriptions

Read, list and delete

Poll for status, read the text, page through everything the key can see, and remove a transcript.

GET/api/v1/transcriptions/{id}scope: transcriptions:read

One transcript: its status while it runs, its content once it is done.

Query parameters

FieldTypeRequiredDescription
includestringNoSet to text for the full transcript body inline. Omitted, you get a 500-character preview and a URL.
formatstringNoOne of srt, vtt, txt, json, docx, pdf. Returns the file itself. See Exports.
speakersbooleanNoLabel each cue with its speaker in srt, vtt and txt, and head each paragraph with the name in docx and pdf. json always carries a speaker field when the transcript has one, so this parameter does not affect it.
timestampsbooleanNodocx and pdf only: head each paragraph with its start time. srt and vtt are timings already, and json carries start and end on every cue.
200 OK
{
  "id": "9f3c8e21-4b7a-4c19-9f2e-1d8a6c3b5e07",
  "title": "The one about interest rates",
  "status": "completed",
  "duration_seconds": 3312,
  "format": "json",
  "source_type": "spotify",
  "source_url": "https://open.spotify.com/episode/4rOoJ…",
  "image_url": "https://i.scdn.co/image/…",
  "show_id": null,
  "created_at": "2026-08-18T09:12:44.117Z",
  "transcript_url": "https://cdn.podcaststotext.com/transcripts/….json",
  "preview": "Welcome back to the show. Today we are talking about…"
}

The body sits behind transcript_url rather than inline, so a status poll does not carry tens of thousands of words you already had.

StatusMeaningWhat to do
processingRunning. Typically a fraction of the episode length.Poll every 5–10s, or use a webhook.
completedDone. transcript_url is populated.Fetch it, or request a format.
failedThe audio could not be transcribed.Check the source URL is still reachable before retrying.

Prefer webhooks to polling. A transcription.completed webhook removes the poll loop entirely, and polling is what the daily request budget is mostly spent on. See Webhooks.

Getting the text

Inline, or as a file
# full body inside the JSON response
curl "https://podcaststotext.com/api/v1/transcriptions/$ID?include=text" \
  -H "Authorization: Bearer $PTT_KEY"

# the transcript as a subtitle file
curl "https://podcaststotext.com/api/v1/transcriptions/$ID?format=vtt" \
  -H "Authorization: Bearer $PTT_KEY" -o episode.vtt

# parameters combine with &: subtitles with each cue labelled
curl "https://podcaststotext.com/api/v1/transcriptions/$ID?format=srt&speakers=true" \
  -H "Authorization: Bearer $PTT_KEY" -o episode.srt

# a Word document headed "Name · 12:34" at each turn
curl "https://podcaststotext.com/api/v1/transcriptions/$ID?format=docx&speakers=true&timestamps=true" \
  -H "Authorization: Bearer $PTT_KEY" -o episode.docx

Quote the URL. In a shell an unquoted & backgrounds the command and the second parameter is lost, so ?format=srt&speakers=true becomes ?format=srt with no error to say so.

A long translation arrives over several calls. Translation is never condensed, it is the whole episode, but a long one does not fit inside a single request. When parts remain, the response is 202 with status: "translating", parts_done and parts_total, and translation: null. POST again with the same language to continue: finished parts are stored and are never charged twice. A 200 with status: "completed" carries the full text.

A very long episode is condensed before the AI tools run on it. An episode small enough to send a model whole is sent whole. One that is not is split into parts, each part condensed into a structured digest, and the digests used in order as a stand-in for the whole episode, so the output covers the end of the episode, not just the beginning. When that happens the response carries source: "digest". Translation is never condensed: it is chunked and translated in full.

Time-coded formats need timestamps. srt, vtt and json need a transcript with segments. Free-tier transcripts are plain text and return 400 for those formats. txt, docx and pdf work for any transcript.

Listing them

GET/api/v1/transcriptionsscope: transcriptions:read

Everything this key’s account can see, newest first, cursor-paginated. No transcript bodies, ever: not even previews.

FieldTypeRequiredDescription
limitnumberNoHow many to return. Default 20, maximum 50.
statusstringNoFilter to processing, completed or failed.
show_idstringNoOnly transcripts belonging to one connected show.
cursorstringNoThe next_cursor from a previous page.
Walk every page
CURSOR=""
while :; do
  PAGE=$(curl -sS "https://podcaststotext.com/api/v1/transcriptions?limit=50${CURSOR:+&cursor=$CURSOR}" \
           -H "Authorization: Bearer $PTT_KEY")
  echo "$PAGE" | jq -r '.data[] | "\(.id)  \(.title)"'
  CURSOR=$(echo "$PAGE" | jq -r '.next_cursor // empty')
  [ -z "$CURSOR" ] && break
done

next_cursor is opaque: pass it back verbatim; null means the end. It identifies a row by created_at and id, so rows created in the same millisecond still page correctly and nothing is skipped or repeated mid-page.

Deleting one

DELETE/api/v1/transcriptions/{id}scope: transcriptions:write

Removes the transcript and its stored files, answering { "id": "…", "deleted": true }. A 404 means no such id or it belongs to another account; the two are not distinguished.

Permanent, and it does not refund. Credits were spent when the audio was transcribed, so deleting the result does not return them, and re-transcribing the same episode spends them again.