GuidesFEEDS

Read a podcast RSS feed in JavaScript

Most podcast hosts let browser JavaScript read their feeds directly: on 28 October 2026, 15 of 21 hosts we tested sent Access-Control-Allow-Origin: *, including Libsyn, Megaphone, Buzzsprout, Spotify for Creators and Transistor. For the other 6, among them Spreaker, Substack and SoundCloud, fetch the feed through the free proxy.cors.dev (no signup, GET and HEAD, about 600 requests a minute per IP), which returns feeds up to 6 MiB. Play episodes with a plain <audio> element, which needs no CORS; 12 of the 14 episode files we checked were larger than the proxy's limit anyway.

Which podcast hosts send CORS headers

When we tested feeds across 21 podcast hosts on 28 October 2026 with an Origin header, 15 of them answered with Access-Control-Allow-Origin: *. You can fetch feeds on those 15 hosts straight from browser JavaScript without a proxy.

The remaining 6 hosts blocked cross-origin reads. Five sent no CORS header at all, and SoundCloud (feeds.soundcloud.com) returned Access-Control-Allow-Origin: https://www.radiofrance.fr regardless of which origin asked, which blocks every other site.

Podcast feed hosts and CORS, checked October 2026
HostFeed domainCORS headerHow to fetch
Libsynrss.libsyn.com*directly
Megaphonefeeds.megaphone.fm*directly
Buzzsproutrss.buzzsprout.com*directly
Spotify for Creatorsanchor.fm*directly
Simplecastfeeds.simplecast.com*directly
Transistorfeeds.transistor.fm*directly
Podbeanfeed.podbean.com*directly
Acastaccess.acast.com*directly
Omny Studioomnycontent.com*directly
Spreakerspreaker.comnonethrough cors.dev up to 6 MiB
Substackapi.substack.comnonethrough cors.dev up to 6 MiB
Squarespaceyour site with ?format=rssnonethrough cors.dev up to 6 MiB
FeedBurnerfeeds.feedburner.comnonethrough cors.dev up to 6 MiB
SoundCloudfeeds.soundcloud.comhttps://www.radiofrance.fr onlythrough cors.dev up to 6 MiB

ART19 (rss.art19.com), Captivate (feeds.captivate.fm), RedCircle (feeds.redcircle.com), RSS.com (media.rss.com), Castos (feeds.castos.com) and Pinecast (pinecast.com) also send *. Blubrry (feeds.blubrry.com) sends no header, and neither does the other Acast feed domain, feeds.acast.com, so fetch those Acast URLs through cors.dev.

Feeds that publishers host themselves vary: the BBC (podcasts.files.bbci.co.uk) sends *, NPR (feeds.npr.org) only allows https://apps.npr.org, and This American Life and Changelog send no CORS header.

Fetch directly, fall back to cors.dev

Because most podcast hosts already send Access-Control-Allow-Origin: *, your code should try a direct fetch first and fall back to https://proxy.cors.dev/ only when the direct call fails. Through https://proxy.cors.dev/, every feed under 6 MiB returned 200 with the caller's origin allowed. To use the free tier, prepend https://proxy.cors.dev/ to the target URL, such as https://proxy.cors.dev/https://www.thisamericanlife.org/podcast/rss.xml (it needs no signup or key, supports GET and HEAD, and allows about 600 requests a minute per IP before returning 429 with a Retry-After header).

In the XML, each episode is an item element containing title, pubDate, guid, enclosure (which carries the attributes url, length in bytes, and type), itunes:duration, and itunes:image. itunes:duration comes as plain seconds (13820 on Acquired), mm:ss (27:24 on Lore) or hh:mm:ss (01:02:07 on This American Life), and getElementsByTagName('itunes:duration') matches prefixed tags by their full name. Show notes are third-party HTML; see fetching RSS feeds in JavaScript for safe rendering.

The podcastEpisodes() snippet below tries a direct fetch first and only falls back to https://proxy.cors.dev/ when that fails; in our tests it returned episodes from Transistor directly and from This American Life through the proxy.

podcastEpisodes(): direct fetch first, cors.dev only when needed
const PROXY = 'https://proxy.cors.dev/';

async function feedXml(feedUrl) {
  // Most hosts send CORS headers, so we only use the proxy when the direct fetch fails
  const direct = await fetch(feedUrl).catch(() => null);
  if (direct?.ok) return direct.text();

  const res = await fetch(PROXY + feedUrl);
  if (!res.ok) throw new Error(`Feed failed: ${res.status} ${res.headers.get('X-Cors-Error') ?? ''}`);
  return res.text();
}

async function podcastEpisodes(feedUrl, limit = 20) {
  const doc = new DOMParser().parseFromString(await feedXml(feedUrl), 'text/xml');
  const first = (el, tag) => el.getElementsByTagName(tag)[0];

  return [...doc.querySelectorAll('item')].slice(0, limit).map((item) => ({
    title: first(item, 'title')?.textContent ?? '',
    published: new Date(first(item, 'pubDate')?.textContent ?? ''),
    duration: first(item, 'itunes:duration')?.textContent ?? '',
    audio: first(item, 'enclosure')?.getAttribute('url') ?? '',
    bytes: Number(first(item, 'enclosure')?.getAttribute('length') ?? 0),
    image: first(item, 'itunes:image')?.getAttribute('href') ?? '',
  }));
}

console.log(await podcastEpisodes('https://www.thisamericanlife.org/podcast/rss.xml', 3));

Redirects and feed size

Browsers check for CORS headers at every hop of a redirect chain, not only on the final response. If any URL along the way omits the header, the browser aborts the request:

  • https://feed.syntax.fm/ responds with 302 and no CORS headers before landing on feeds.megaphone.fm, which sends *. A direct fetch of the vanity URL fails in the browser, so fetch the final Megaphone URL directly instead.
  • feeds.fireside.fm/linuxunplugged/rss sends *, but it redirects to feeds.jupiterbroadcasting.com/lup, which sends no CORS header.
  • An old audioboom.com feed URL redirected without CORS headers through ART19 and on to Simplecast after the show switched hosts.

When you go through https://proxy.cors.dev/, the proxy follows up to 20 redirects on its side and returns the final response with CORS headers attached.

Feed size grows with the back catalog: Megaphone's The Rest Is History was 3,349,303 bytes for 735 episodes, and Libsyn's Lore 2,076,948 bytes for 500. The free proxy returns up to 6 MiB (6,291,456 bytes) per response within a 30-second deadline; larger feeds fail with HTTP 502 and X-Cors-Error: response_too_large.

Eight of the 36 feeds we fetched were over that limit, from 7,430,633 bytes (a Buzzsprout show) to 13,005,257 bytes (an Omny Studio show). Six of the 8 are served by hosts that send *, so fetch their final URL directly; a direct fetch has no size limit. The other 2, a Spreaker show (8,363,381 bytes) and the self-hosted Jupiter Broadcasting feed (9,394,471 bytes), are too large for the proxy and send no CORS header, so read them from your own server. Changelog's feed (6,278,794 bytes, no CORS) just fit and loaded through the proxy.

Play episodes without proxying audio

An HTML audio element plays cross-origin audio files without needing any CORS headers. You only need CORS on the final file host if you read the raw audio bytes from JavaScript, such as passing the URL to fetch, calling Web Audio's decodeAudioData, or drawing a waveform.

We tested 14 episode files: 12 were over 6 MiB, with sizes spanning 0.5 MiB to 248.3 MiB, including one Acquired episode that is 221,157,617 bytes long (13,820 seconds). Those enclosure URLs went through 1 to 6 redirects across measurement services like dts.podtrac.com, pdst.fm, and pscrb.fm before reaching the file host, and some final hosts (delivery-edge.libsyn.com, audio.buzzsprout.com) sent no CORS header.

Routing a full audio download through https://proxy.cors.dev/ fails with 502 and X-Cors-Error: response_too_large. Never send audio through the proxy: assign the enclosure url straight to the src attribute of an audio element, and set preload="none" on each audio element in an episode list so the browser doesn't start downloading every file at once.

Find a show's feed URL

If you only have a show's name or Apple ID, you can look up its feed URL from the browser using Apple's APIs:

  1. Search or look up the show on Apple directly

    Call https://itunes.apple.com/search?media=podcast&term=<term> or https://itunes.apple.com/lookup?id=<Apple podcast ID>. Both endpoints send Access-Control-Allow-Origin: *, need no API key, and return the show's feedUrl (for Acquired, ID 1050462261, the lookup response is 1,598 bytes and returns https://feeds.transistor.fm/acquired).

  2. Do not proxy the Apple request

    When we called Apple through cors.dev, the search endpoint returned 429 and an episode lookup 403, because Apple throttles the proxy's shared IP addresses. The Apple Search API documentation notes a limit of about 20 calls per minute, so call Apple directly from the user's browser.

  3. Fetch the returned feed URL

    Pass feedUrl into your direct-first fetch helper and wire the first item's enclosure into your player.

Find a feed with Apple's lookup API and play the latest episode
async function findFeedUrl(appleId) {
  // Apple's lookup API sends CORS headers, so call it directly
  const res = await fetch(`https://itunes.apple.com/lookup?id=${appleId}`);
  if (!res.ok) throw new Error(`Apple lookup answered ${res.status}`);
  const { results } = await res.json();
  return results[0]?.feedUrl ?? null;
}

const feedUrl = await findFeedUrl('1050462261');
const [latest] = await podcastEpisodes(feedUrl, 1);

// An audio element plays cross-origin files without CORS, so the audio never goes through the proxy
const player = document.createElement('audio');
player.controls = true;
player.preload = 'none';
player.src = latest.audio;
document.body.append(player);

Podcast Index (api.podcastindex.org) also sends Access-Control-Allow-Origin: *, but every request needs an Authorization hash built with SHA-1 from your API key, your API secret and the current Unix time, so the secret would sit in your page. Call it from a server.

Good questions.

What happens if a feed is larger than 6 MiB?

The free proxy rejects responses over 6 MiB (6,291,456 bytes) with HTTP 502 and X-Cors-Error: response_too_large. Most large feeds we found sit on hosts that send Access-Control-Allow-Origin: *, such as Buzzsprout and Omny Studio, so fetch those directly with no size limit. A feed that is over 6 MiB and has no CORS header has to be fetched by your own server.

Why does a vanity feed URL fail when the underlying host allows CORS?

Browsers check Access-Control-Allow-Origin on every redirect, not just the final response, and https://feed.syntax.fm/ answers 302 without it before reaching feeds.megaphone.fm, which sends *. Fetch the final Megaphone URL directly, or route the vanity URL through https://proxy.cors.dev/, which follows up to 20 redirects on its side.

When do I need CORS headers on the podcast audio file itself?

You don't need CORS headers for standard playback when you set the episode's enclosure URL as the src of an HTML audio element. You only need CORS on the final audio host if you read the audio bytes in JavaScript using fetch, Web Audio's decodeAudioData, or a waveform renderer. 12 of the 14 episodes we tested exceeded 6 MiB and hosts like delivery-edge.libsyn.com and audio.buzzsprout.com send no CORS headers, so reading whole files fails both directly and through the proxy; process audio on a server instead.

Can I call the Podcast Index API from browser JavaScript through cors.dev?

No, call api.podcastindex.org from your own server. It already sends Access-Control-Allow-Origin: * and allows Authorization, X-Auth-Date and X-Auth-Key in its preflight, so CORS isn't the obstacle. Every request needs a User-Agent naming your app and an Authorization hash built with SHA-1 from your API key, your API secret and the X-Auth-Date Unix time (accepted within 3 minutes), and that secret would be readable by anyone inspecting your page.

Do I need an API key to read a podcast RSS feed?

No. Podcast feeds are public XML files, and 15 of the 21 hosts we tested let any website read them with a plain fetch(). For the rest, https://proxy.cors.dev/ needs no signup or key, allows about 600 requests a minute per IP with no daily quota, and returns feeds up to 6 MiB. Apple's lookup and search APIs need no key either; Podcast Index does.