PodcastsToText
Sources

One URL, any source

The single field that accepts every platform, how each one is resolved, and the two caveats worth knowing first.

One field takes every source. You do not detect the platform, pick an endpoint, or resolve an episode page to an audio file: send the link a human would paste.

The same call, five different sources
POST /api/v1/transcriptions   {"url": "https://open.spotify.com/episode/4rOoJ…"}
POST /api/v1/transcriptions   {"url": "https://podcasts.apple.com/us/podcast/…"}
POST /api/v1/transcriptions   {"url": "https://www.youtube.com/watch?v=…"}
POST /api/v1/transcriptions   {"url": "https://www.tiktok.com/@user/video/…"}
POST /api/v1/transcriptions   {"url": "https://traffic.libsyn.com/show/ep42.mp3"}

How a URL is routed

If the URL containsWeResult
spotify.com, spotify.linkFind the show’s RSS feed and match the episodeDirect audio file
podcasts.apple.com, apple.coResolve the show to its feed and match the episodeDirect audio file
youtube.com, youtu.beRead the existing caption trackTranscript, no audio downloaded
tiktok.comExtract and stage the audioDirect audio file
anything else ending .mp3 .m4a .wav .aac .ogg .opus .flac .webm .mp4 .movTake it at face valueDirect audio file
anything elseRefuse400 bad_request

The create response’s source_type (spotify, apple, youtube, tiktok, direct, upload) says what it decided, which is worth logging. If you already hold a direct audio URL, send it as audio_url instead of url and no resolution runs at all.

YouTube: caption tracks only

YouTube links are transcribed from the video’s existing caption track. No audio is downloaded, no transcription minutes are spent, and the job finishes inline, the create response already says "status": "completed". Auto-generated captions are inherited verbatim, mistakes included; speaker labels and timings are only as good as the track.

No caption track is a 422, and retrying will not fix it. A video with captions disabled returns 422 source_unavailable. That is a property of the video, not of your request, pick a video with captions, or send the audio as audio_url.

Spotify and Apple Podcasts: resolved through RSS

Neither platform serves audio to third parties, so we find the show’s public RSS feed, match the episode, and transcribe the file the publisher hosts. spotify.link and apple.co short links work. On Apple the ?i= parameter is what identifies the episode; a link without it points at the show.

CauseWhat you see
Spotify Original or ExclusiveNo public RSS feed exists, so there is no audio to fetch. 400 bad_request, and no third-party tool can reach it either.
A show link rather than an episode linkNo single episode to transcribe. Use the lookup calls below.
Subscriber-gated or removed episodeThe platform page exists; the audio is behind authentication we do not hold, or gone from the feed.

Show → feed → episodes

GET/api/v1/podcasts/feedsscope: transcriptions:read
GET/api/v1/podcasts/lookupscope: transcriptions:read
Find a feed, then list its episodes
# find a feed by name
curl "https://podcaststotext.com/api/v1/podcasts/feeds?q=Hard%20Fork" \
  -H "Authorization: Bearer $PTT_KEY"

# list episodes from a feed, with audio URLs
curl "https://podcaststotext.com/api/v1/podcasts/lookup?url=https://feeds.simplecast.com/…" \
  -H "Authorization: Bearer $PTT_KEY"

Then post each episode’s audio URL as audio_url: much faster across a back catalogue, and mind the concurrency cap.

TikTok

Send the video link. Extraction runs on a separate worker that downloads the video, pulls the audio out of it and re-hosts it. TikTok’s CDN refuses a plain fetch from a serverless host, so nothing about the download happens from the API’s own IP. It takes a few seconds and then transcribes like any other audio; source_type comes back tiktok.

A failure here is usually transient, the extractor was at capacity, or the download outran the request budget, so a retry is worth one attempt, unlike the YouTube case above. A video that fails repeatedly is private, removed or region-locked; passing the audio yourself as audio_url is the way round that.

Connected shows

GET/api/v1/showsscope: shows:read

The feeds this account has connected, newest first, and where the show_id that filters the list endpoint comes from. No parameters, and shows:read is a read scope: included wherever the API is, with no extra permission needed.

200 OK
{
  "data": [
    {
      "id": "b41c…",
      "title": "Hard Fork",
      "author": "The New York Times",
      "rss_url": "https://feeds.simplecast.com/l2i9YnTd",
      "website_url": "https://www.nytimes.com/column/hard-fork",
      "image_url": "https://image.simplecastcdn.com/…jpg",
      "verified": true,
      "auto_transcribe": false,
      "last_checked_at": "2026-09-08T04:00:11Z",
      "created_at": "2026-07-02T18:20:03Z"
    }
  ],
  "next_actions": [
    "GET /api/v1/transcriptions?show_id={id} for a show's transcripts"
  ]
}
FieldWhat it decides
verifiedWhether ownership of the feed has been proved. Publishing requires it. Verify at your sources page.
auto_transcribeWhether new episodes in this feed are transcribed automatically: they then arrive without an API call and spend the plan allowance on their own.

Connecting a feed is a dashboard action. There is no POST /api/v1/shows: the ownership verification that stops a key claiming someone else’s feed is a human step.