If you built a podcast product in the last few years, there is a good chance it talks to the Listen API from ListenNotes. It is a well-documented, well-run REST API, and its endpoint design has become a de facto standard for podcast search. That is exactly why switching providers is usually painful: your code, your data model and your tests are shaped around one vendor's response format.
podsearch.org takes a different approach. Instead of inventing a new schema, it exposes a ListenNotes-compatible REST API at https://podsearch.org/api/v2. The endpoints have the same paths, the same query parameters and the same response field names, and requests authenticate with the same X-ListenAPI-Key header. For most applications, migrating is a matter of changing a base URL and an API key, then running your test suite.
This guide walks through the whole process: why teams migrate, how the pricing compares, what is identical, what is different, and a step-by-step plan for switching without breaking production.
Why teams look for a ListenNotes alternative
Nobody migrates an API for fun. The reasons we hear most often fall into four groups.
Cost at scale. API spend grows with your user base. A search box that fires on every keystroke, a recommendation widget on every episode page, or a nightly job that refreshes thousands of podcasts can push request volume well past the included quota of any plan. When the overage rate is the dominant line item, halving it matters.
Catalog coverage and openness. podsearch.org is built on the Podcast Index catalog, the open, community-run index of roughly 4.7 million RSS feeds. Around 3.2 million active feeds are parsed into episodes and indexed for full-text search. Because the underlying catalog is open, you can always cross-check a result against the public RSS feed it came from.
Freshness. For apps that care about new episodes appearing quickly (news, sports, daily shows), the time between "publisher hits publish" and "episode is searchable" matters. podsearch.org listens to Podping, a notification bus on the Hive blockchain that hosting platforms use to announce feed updates, and re-polls those feeds within seconds to minutes. Feeds that do not send Podpings are polled on an adaptive schedule based on how often they publish.
Podcast 2.0 data. Many feeds now carry Podcast Namespace tags such as <podcast:transcript>, <podcast:chapters>, <podcast:soundbite> and <podcast:person>. podsearch.org parses these where feeds provide them, which opens up transcript search and people-based queries.
None of this is a criticism of ListenNotes. It is a mature product with features we do not have, and for some teams it remains the right choice. The point of a compatible API is that you can make that decision on price and fit rather than on the cost of rewriting your integration.
Pricing compared, as of October 2026
podsearch.org API plans are set at exactly half of the ListenNotes published API prices. The ListenNotes figures below come from the Listen API pricing page, checked on 2026-10-24.
| Plan | ListenNotes (as of Oct 2026) | podsearch.org |
|---|---|---|
| Free | $0, 150 requests/month, 2 req/s and 20 req/min, up to 30 results per query | $0, 150 requests/month, 2/s, 20/min, 30 results per query |
| Pro base | $200/month, 5,000 requests included | $100/month, 5,000 requests included |
| Overage to 1M requests | $1.60 per 1,000 | $0.80 per 1,000 |
| Overage 1M to 5M | $1.30 per 1,000 | $0.65 per 1,000 |
| Overage above 5M | $1.10 per 1,000 | $0.55 per 1,000 |
| Pro results per query | up to 300 | 300 |
| Pro rate limit | "Medium" (no number published) | 300 requests/minute |
| Enterprise | Custom quote, billed annually | 50% of your ListenNotes Enterprise quote, annual |
A worked example makes the difference concrete. Suppose your app makes 250,000 requests a month. On ListenNotes Pro that is the $200 base, plus 245,000 overage requests at $1.60 per 1,000, which is $392, for a total of $592 a month. On podsearch.org Pro it is $100 plus 245 × $0.80 = $196, for a total of $296 a month. Over a year, the difference is $3,552, and the gap widens as volume grows.
ListenNotes Pro also includes things that are not part of a request quota: up to 10 team members, transcripts, advanced data fields such as listen scores and global rank, and priority support, according to its pricing page. Compare the full feature list, not only the price, before you decide.
What "ListenNotes-compatible" means in practice
Compatibility is a promise about three things: the shape of requests, the shape of responses, and authentication.
Same endpoints
The common Listen API v2 endpoints exist under the same paths:
GET /searchfor full-text episode and podcast searchGET /typeaheadfor autocomplete suggestionsGET /podcasts/{id}for a podcast and a page of its episodesGET /episodes/{id}for a single episodePOST /podcastsandPOST /episodesfor batch lookups (up to 10 IDs per call)GET /best_podcastsfor curated, genre-filtered listsGET /podcasts/{id}/recommendationsandGET /episodes/{id}/recommendationsGET /genres,GET /regions,GET /languagesGET /just_listen,GET /curated_podcasts,GET /trending_searches,GET /related_searches,GET /spellcheck
Same parameters
The /search endpoint accepts the parameters you already use: q, type (episode or podcast), offset, sort_by_date, len_min, len_max, genre_ids, published_before, published_after (both epoch milliseconds), only_in, language, safe_mode and unique_podcasts. Pagination works the same way: each response carries next_offset, and you pass it back as offset to fetch the next page.
Same response fields
Episode results contain the field names your parser expects: id, title_original, title_highlighted, description_original, pub_date_ms, audio, audio_length_sec, explicit_content, image, thumbnail, link, guid_from_rss and a nested podcast object with id, title_original, publisher_original, image and rss. Podcast objects carry id, title, publisher, image, description, rss, website, genre_ids, total_episodes and latest_pub_date_ms. Podcast-type search results use these podcast field names (title, publisher), so if your parser reads title_original on podcast search results, add a fallback to title.
Same authentication
Every request sends the key in the X-ListenAPI-Key header. There is no request signing and no OAuth dance. If your HTTP client already sets that header from an environment variable, you only need to change the value.
A minimal before-and-after
Here is a typical search call against ListenNotes:
curl -s "https://listen-api.listennotes.com/api/v2/search?q=climate%20policy&type=episode" \
-H "X-ListenAPI-Key: $LISTEN_API_KEY"
And the same call against podsearch.org:
curl -s "https://podsearch.org/api/v2/search?q=climate%20policy&type=episode" \
-H "X-ListenAPI-Key: $PODSEARCH_API_KEY"
In application code, the cleanest approach is to make the base URL configuration rather than a constant:
import os
import requests
BASE_URL = os.environ.get("PODCAST_API_BASE", "https://podsearch.org/api/v2")
API_KEY = os.environ["PODCAST_API_KEY"]
session = requests.Session()
session.headers["X-ListenAPI-Key"] = API_KEY
def search_episodes(query, offset=0, **filters):
params = {"q": query, "type": "episode", "offset": offset, **filters}
r = session.get(f"{BASE_URL}/search", params=params, timeout=15)
r.raise_for_status()
return r.json()
page = search_episodes("climate policy", language="en")
for ep in page["results"]:
print(ep["pub_date_ms"], ep["podcast"]["title_original"], "-", ep["title_original"])
print("next offset:", page["next_offset"])
In JavaScript the pattern is the same:
const BASE_URL = process.env.PODCAST_API_BASE ?? "https://podsearch.org/api/v2";
async function getPodcast(id, nextEpisodePubDate) {
const url = new URL(`${BASE_URL}/podcasts/${id}`);
if (nextEpisodePubDate) url.searchParams.set("next_episode_pub_date", nextEpisodePubDate);
const res = await fetch(url, { headers: { "X-ListenAPI-Key": process.env.PODCAST_API_KEY } });
if (!res.ok) throw new Error(`podcast ${id}: HTTP ${res.status}`);
return res.json();
}
What is different, and how to handle it
Compatible does not mean identical. Being upfront about the differences will save you a debugging session.
IDs are not interchangeable
ListenNotes uses its own opaque string IDs. podsearch.org uses Podcast Index feed IDs for podcasts and its own numeric IDs for episodes, returned as strings. If you have stored ListenNotes IDs in your database, they will not resolve on podsearch.org. The reliable bridge is the RSS feed URL, which both providers expose as rss. POST /podcasts accepts a comma-separated rss form field, so you can look up your stored shows by feed URL and record the new IDs:
curl -s -X POST "https://podsearch.org/api/v2/podcasts" \
-H "X-ListenAPI-Key: $PODSEARCH_API_KEY" \
--data-urlencode "rss=https://feeds.example.com/show-one.xml,https://feeds.example.com/show-two.xml"
For episodes, match on guid_from_rss plus the podcast's feed URL. The RSS <guid> is set by the publisher and is the most stable identifier an episode has.
Some ListenNotes-specific fields are empty
ListenNotes computes proprietary metrics such as the Listen Score and global rank. podsearch.org returns those keys so your parser does not crash, but their values are null. If your UI depends on them, decide what to show instead: episode count, recency, or chart presence from the Apple Podcasts charts that podsearch.org tracks per country.
Extra parameters you can opt into
podsearch.org adds a few optional parameters on /search. They are ignored by code that does not send them, so they never break compatibility:
fq_podcast_title,fq_episode_title,fq_descriptionandfq_contentrestrict a term to one field.syntax=dqlturns on boolean query syntax withAND,OR,NOT, parentheses, quoted phrases and wildcards.deep_search=1includes episode content and transcripts in matching, where feeds provide them.exclude_ai=truedrops results from shows that an LLM classifier has judged likely AI-generated with at least 0.9 confidence. Coverage of that classifier is partial; a show with no label has simply not been judged yet.recency=1boosts newer episodes in relevance order, andquality=1prefers established shows.
Plan limits
Results per query and rate limits follow the plan table above: 30 results and 2 requests per second on Free; 300 results and 300 requests per minute on Pro. Batch endpoints are a Pro feature, as they are on ListenNotes.
A step-by-step migration plan
The safest migrations are boring. Here is a plan that keeps production stable.
- Inventory your calls. Grep your code for the ListenNotes base URL and list every endpoint and parameter you use. Most apps use five or fewer endpoints.
- Make the base URL configurable. Move the hostname and key into environment variables so you can switch per environment, or per request, without a deploy.
- Get a key. Sign up on the developers page. The Free plan is enough to run the steps below.
- Record a golden set. Pick 50 to 100 real queries from your logs, including a few long-tail and non-English ones. Save the ListenNotes responses.
- Run a shadow comparison. Send the same queries to podsearch.org and compare. Do not expect identical rankings; the two services use different ranking signals. Check that the shows you expect appear in the first page, that fields parse, and that pagination terminates.
- Re-key stored IDs. For every podcast you store, resolve the new ID through
POST /podcastswithrss. Keep both IDs during the transition. - Canary. Route a small percentage of traffic, or one internal build, to podsearch.org. Watch error rates and latency.
- Cut over and keep a fallback. Flip the default. Keep the old configuration around for a release cycle so you can roll back with a config change.
A quick shadow-comparison script
This script runs a list of queries against both providers and reports how many of the top ten podcast titles overlap. It is a crude metric, but it surfaces broken queries fast.
import os, requests
PROVIDERS = {
"listennotes": ("https://listen-api.listennotes.com/api/v2", os.environ["LN_KEY"]),
"podsearch": ("https://podsearch.org/api/v2", os.environ["PS_KEY"]),
}
def top_titles(base, key, q):
r = requests.get(f"{base}/search", params={"q": q, "type": "podcast"},
headers={"X-ListenAPI-Key": key}, timeout=15)
r.raise_for_status()
return [(p.get("title_original") or p.get("title") or "").lower()
for p in r.json()["results"][:10]]
for q in open("golden_queries.txt").read().splitlines():
a = top_titles(*PROVIDERS["listennotes"], q)
b = top_titles(*PROVIDERS["podsearch"], q)
overlap = len(set(a) & set(b))
flag = " <-- review" if overlap < 3 else ""
print(f"{overlap:2d}/10 {q}{flag}")
Testing checklist
Before you flip the default, confirm each of these in a staging build:
- Search returns results for your top 20 queries and an empty, well-formed response for nonsense queries.
next_offsetpagination stops cleanly when there are no more results (it returns-1).- Podcast pages paginate episodes with
next_episode_pub_date. - Audio URLs in
audioplay in your player. They point at the publisher's own enclosure, exactly as in the RSS feed. - Your UI tolerates
nullin fields that podsearch.org does not compute. - Rate-limit responses (HTTP 429) are retried with backoff rather than shown to users.
- Cached responses are invalidated or re-keyed after you change IDs.
Frequently asked questions
Do I need to change my response parsing code?
Usually not. Field names match the Listen API v2 format. The changes most apps need are the base URL, the API key, and a re-mapping of any stored IDs, because IDs differ between the two services.
Is the catalog the same as ListenNotes'?
No. podsearch.org is built from the open Podcast Index catalog of about 4.7 million RSS feeds, with around 3.2 million active feeds parsed into episodes. ListenNotes maintains its own catalog. Coverage overlaps heavily for established shows, but you should test the long tail that matters to your users.
Can I run both providers at once?
Yes, and during a migration you should. Because both use the same request shape, a thin wrapper can route each request to either base URL. Some teams keep a second provider permanently as a fallback.
Are transcripts available?
Where a feed publishes a <podcast:transcript> tag, podsearch.org ingests it. GET /episodes/{id}?show_transcript=1 returns the text when one is stored, and deep_search=1 on /search includes transcript text in matching. Many feeds do not publish transcripts, so treat this as partial coverage.
How is Enterprise priced?
Enterprise is priced at 50% of the quote you have from ListenNotes for Enterprise, billed annually. Share your current quote and requirements and we will match the terms at half the price.
What happens if I exceed the Free plan?
The Free plan includes 150 requests a month at 2 per second and 20 per minute. Once you need more, the Pro plan starts at $100 a month with 5,000 requests included and metered overage after that.
Summary
Migrating off the Listen API does not have to mean a rewrite. Because podsearch.org mirrors the v2 endpoints, parameters, response fields and authentication header, the work is mostly configuration: change the base URL, swap the key, re-key stored IDs through feed URLs, and validate with a shadow comparison. You get the open Podcast Index catalog, Podping-driven freshness and Podcast 2.0 data, at half the published ListenNotes price for every tier.