Skip to content

API Reference (v1)

The PodcasterPlus public API is a keyless, read-only JSON API for published podcast content. This page documents every v1 endpoint and the conventions they all share.

Base URL: https://api.podcasterplus.comVersion prefix: /v1Methods: GET, HEAD, and OPTIONS only

Follow the request conventions

Response envelope

Every response uses a consistent envelope:

json
// Single resource
{ "data": { ... } }

// List
{ "data": [ ... ], "pagination": { "next_cursor": "…" } }

// Error
{ "error": { "code": "not_found", "message": "Podcast not found." } }

Field names are snake_case throughout. Timestamps are ISO 8601 strings in UTC (e.g. "2026-07-01T06:00:00.000Z").

Error codes

Errors carry a stable machine-readable code alongside a human-readable message. Match on the code, not the message text.

HTTP statuserror.codeWhen
400invalid_requestMalformed input: bad slug, invalid UUID, bad cursor, missing parameter
404not_foundThe resource doesn't exist or isn't publicly visible
500internal_errorSomething went wrong on our side
501not_implementedA requested variant isn't supported (e.g. oEmbed format=xml)
503service_unavailableThe API is temporarily unavailable; retry with backoff

One more status can appear: 429 when fair-use rate limiting trips. Rate limiting happens at the platform edge in front of the API, not in the API itself, so a 429 response may not carry the JSON error envelope. Back off and retry with jitter.

Pagination

List endpoints use opaque cursor pagination:

  • limit: items per page. Default 20, maximum 50. Integer values outside the range are clamped; non-integer values return invalid_request.
  • cursor: the pagination.next_cursor value from the previous page. Treat it as an opaque token; a tampered or malformed cursor returns invalid_request.

When next_cursor is null, you've reached the last page.

bash
# First page
curl "https://api.podcasterplus.com/v1/podcasts/acme-radio-hour/episodes?limit=20"

# Next page
curl "https://api.podcasterplus.com/v1/podcasts/acme-radio-hour/episodes?limit=20&cursor=MjAyNi0wNi0wM1QwNjowMDowMC4wMDBafDhhMWYzYzVlLTJiNGQtNGY2YS05YzhlLTdkNWIzYTFmOWUyYw"

Episodes are ordered newest-first (by published_at, then id). Cursor pagination is stable even while new episodes are published: you'll never see duplicates or gaps mid-walk.

Caching and conditional requests

Successful responses are cacheable and carry a strong validator:

Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=600
ETag: "a1b2c3d4e5f6…"

Send the ETag back in If-None-Match to get a bandwidth-free 304 Not Modified when nothing changed:

bash
curl -i https://api.podcasterplus.com/v1/podcasts/acme-radio-hour \
  -H 'If-None-Match: "a1b2c3d4e5f6…"'
# HTTP/2 304

Please respect the cache headers: content changes at podcast-publishing cadence, so polling faster than once a minute buys you nothing.

CORS

The API is callable directly from browsers on any origin:

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, HEAD, OPTIONS
Access-Control-Max-Age: 86400

No credentials are used or accepted, which is exactly why the open CORS policy is safe.

Authentication

None. v1 read endpoints are keyless. An Authorization: Bearer pp_live_… / pp_test_… scheme is reserved for future keyed endpoints; if you send it to v1 read endpoints it is silently ignored, never an error.

Input validation

Validation happens before any lookup:

  • Podcast and episode slugs: lowercase letters, digits, and hyphens; 1–100 characters; must start with a letter or digit.
  • Episode IDs: UUIDs.

Invalid input returns 400 invalid_request; valid-but-unknown input returns 404 not_found.


Endpoints

GET /v1

API index. Returns the API name, version, and a link to these docs.

bash
curl https://api.podcasterplus.com/v1
json
{
	"data": {
		"name": "PodcasterPlus Public API",
		"version": "v1",
		"documentation": "https://docs.podcasterplus.com/developers/"
	}
}

GET / returns the same body.

GET /v1/podcasts/

Show metadata for one podcast.

bash
curl https://api.podcasterplus.com/v1/podcasts/acme-radio-hour
json
{
	"data": {
		"id": "b7e4c1a0-5f2d-4e8a-9c3b-1d6f8a2e4c90",
		"slug": "acme-radio-hour",
		"guid": "9d2c4f6a-1b3e-5d7c-8f9a-0b2d4e6f8a1c",
		"title": "The Acme Radio Hour",
		"subtitle": "Weekly stories from the workshop",
		"author": "Jess Doe",
		"description": "Interviews and field stories from people who build things for a living.",
		"language": "en",
		"category": "Technology",
		"subcategory": "Tech News",
		"explicit": false,
		"artwork": {
			"url": "https://media.podcasterplus.com/podcasts/b7e4c1a0-5f2d-4e8a-9c3b-1d6f8a2e4c90/images/cover.jpg"
		},
		"brand_color": "#7E22CE",
		"player_color": null,
		"website_url": "https://acmeradiohour.com",
		"episode_count": 42,
		"links": {
			"listen": "https://listen.podcasterplus.com/acme-radio-hour",
			"feed": "https://feed.podcasterplus.com/acme-radio-hour"
		},
		"branding": {
			"show_powered_by": true
		}
	}
}

Podcast object

FieldTypeNotes
idstring (UUID)Stable podcast identifier
slugstringURL slug; podcasters can change this, so prefer id for stable references
guidstring | nullThe show's permanent podcast:guid (Podcast 2.0)
titlestring
subtitlestring | null
authorstring | null
descriptionstring | nullPlain text; HTML is stripped server-side
languagestring | nulle.g. "en"
category / subcategorystring | nullApple Podcasts category taxonomy
explicitboolean
artwork.urlstring | nullShow cover art
brand_colorstring | nullThe show's brand colour as a hex string
player_colorstring | nullThe show's player accent colour (#rrggbb); null = the default purple
website_urlstring | null
episode_countnumberCount of publicly visible (published) episodes
links.listenstringPublic listen page
links.feedstringPublic RSS feed
branding.show_powered_bybooleanWhether embedded players display "Powered by PodcasterPlus"

Errors: 400 invalid_request (bad slug), 404 not_found (unknown, unpublished, or externally hosted show).

GET /v1/podcasts/{slug}/episodes

Paginated list of a show's published episodes, newest first.

Query parameters: limit (default 20, max 50), cursor (opaque; see Pagination).

bash
curl "https://api.podcasterplus.com/v1/podcasts/acme-radio-hour/episodes?limit=2"
json
{
	"data": [
		{
			"id": "3f9d2b6e-8c41-4a57-b2f0-6e5d4c3b2a19",
			"slug": "the-big-launch",
			"title": "The Big Launch",
			"description": "We finally ship the thing, and talk through everything that broke on the way.",
			"season_number": 2,
			"episode_number": 14,
			"episode_type": "full",
			"explicit": false,
			"published_at": "2026-07-01T06:00:00.000Z",
			"audio": {
				"url": "https://media.podcasterplus.com/podcasts/b7e4c1a0-5f2d-4e8a-9c3b-1d6f8a2e4c90/e/3f9d2b6e-8c41-4a57-b2f0-6e5d4c3b2a19/audio/original.mp3",
				"content_type": "audio/mpeg",
				"duration_seconds": 1843,
				"size_bytes": 29488123
			},
			"artwork": {
				"url": "https://media.podcasterplus.com/podcasts/b7e4c1a0-5f2d-4e8a-9c3b-1d6f8a2e4c90/images/cover.jpg"
			},
			"links": {
				"listen": "https://listen.podcasterplus.com/acme-radio-hour/e/the-big-launch",
				"embed": "https://embed.podcasterplus.com/ep/3f9d2b6e-8c41-4a57-b2f0-6e5d4c3b2a19"
			}
		},
		{
			"id": "8a1f3c5e-2b4d-4f6a-9c8e-7d5b3a1f9e2c",
			"slug": "prototypes-and-regrets",
			"title": "Prototypes and Regrets",
			"description": "Three prototypes, two regrets, one working demo.",
			"season_number": 2,
			"episode_number": 13,
			"episode_type": "full",
			"explicit": false,
			"published_at": "2026-06-03T06:00:00.000Z",
			"audio": {
				"url": "https://media.podcasterplus.com/podcasts/b7e4c1a0-5f2d-4e8a-9c3b-1d6f8a2e4c90/e/8a1f3c5e-2b4d-4f6a-9c8e-7d5b3a1f9e2c/audio/original.mp3",
				"content_type": "audio/mpeg",
				"duration_seconds": 2210,
				"size_bytes": 35361040
			},
			"artwork": {
				"url": "https://media.podcasterplus.com/podcasts/b7e4c1a0-5f2d-4e8a-9c3b-1d6f8a2e4c90/e/8a1f3c5e-2b4d-4f6a-9c8e-7d5b3a1f9e2c/images/cover.jpg"
			},
			"links": {
				"listen": "https://listen.podcasterplus.com/acme-radio-hour/e/prototypes-and-regrets",
				"embed": "https://embed.podcasterplus.com/ep/8a1f3c5e-2b4d-4f6a-9c8e-7d5b3a1f9e2c"
			}
		}
	],
	"pagination": {
		"next_cursor": "MjAyNi0wNi0wM1QwNjowMDowMC4wMDBafDhhMWYzYzVlLTJiNGQtNGY2YS05YzhlLTdkNWIzYTFmOWUyYw"
	}
}

Episode list item

FieldTypeNotes
idstring (UUID)Stable episode identifier, safe to reference from embeds
slugstringEpisode URL slug (can change; id never does)
titlestring
descriptionstring | nullPlain text; HTML is stripped server-side
season_numbernumber | null
episode_numbernumber | null
episode_typestring | null"full", "trailer", or "bonus"
explicitboolean
published_atstringISO 8601 UTC
audio.urlstring | nullDirect audio URL, playable cross-origin in a plain <audio> element
audio.content_typestring | nulle.g. "audio/mpeg"
audio.duration_secondsnumber | null
audio.size_bytesnumber | null
artwork.urlstring | nullEpisode cover art, falling back to the show's cover art
links.listenstringPublic episode page: https://listen.podcasterplus.com/{slug}/e/{episodeSlug}
links.embedstringIframe player page: https://embed.podcasterplus.com/ep/{id}

Errors: 400 invalid_request (bad slug, non-integer limit, bad cursor), 404 not_found (unknown show).

GET /v1/podcasts/{slug}/episodes/latest

The show's newest published episode, in the full episode shape. This powers the player's latest-episode mode.

bash
curl https://api.podcasterplus.com/v1/podcasts/acme-radio-hour/episodes/latest

Errors: 400 invalid_request (bad slug), 404 not_found (unknown show, or a show with no published episodes yet).

GET /v1/podcasts/{slug}/episodes/

One episode of a show, addressed by either its UUID or its episode slug. Returns the full episode shape.

bash
curl https://api.podcasterplus.com/v1/podcasts/acme-radio-hour/episodes/the-big-launch
curl https://api.podcasterplus.com/v1/podcasts/acme-radio-hour/episodes/3f9d2b6e-8c41-4a57-b2f0-6e5d4c3b2a19

When addressed by UUID, the episode must belong to the podcast in the path: a UUID under the wrong show's slug returns 404 not_found.

Errors: 400 invalid_request (bad podcast slug or episode reference), 404 not_found.

GET /v1/episodes/

One episode by UUID alone, the player's one-request lookup. Returns everything a player needs, including a summary of the parent podcast.

bash
curl https://api.podcasterplus.com/v1/episodes/3f9d2b6e-8c41-4a57-b2f0-6e5d4c3b2a19
json
{
	"data": {
		"id": "3f9d2b6e-8c41-4a57-b2f0-6e5d4c3b2a19",
		"slug": "the-big-launch",
		"title": "The Big Launch",
		"description": "We finally ship the thing, and talk through everything that broke on the way.",
		"season_number": 2,
		"episode_number": 14,
		"episode_type": "full",
		"explicit": false,
		"published_at": "2026-07-01T06:00:00.000Z",
		"audio": {
			"url": "https://media.podcasterplus.com/podcasts/b7e4c1a0-5f2d-4e8a-9c3b-1d6f8a2e4c90/e/3f9d2b6e-8c41-4a57-b2f0-6e5d4c3b2a19/audio/original.mp3",
			"content_type": "audio/mpeg",
			"duration_seconds": 1843,
			"size_bytes": 29488123
		},
		"artwork": {
			"url": "https://media.podcasterplus.com/podcasts/b7e4c1a0-5f2d-4e8a-9c3b-1d6f8a2e4c90/images/cover.jpg"
		},
		"links": {
			"listen": "https://listen.podcasterplus.com/acme-radio-hour/e/the-big-launch",
			"embed": "https://embed.podcasterplus.com/ep/3f9d2b6e-8c41-4a57-b2f0-6e5d4c3b2a19"
		},
		"podcast": {
			"id": "b7e4c1a0-5f2d-4e8a-9c3b-1d6f8a2e4c90",
			"slug": "acme-radio-hour",
			"title": "The Acme Radio Hour",
			"author": "Jess Doe",
			"artwork": {
				"url": "https://media.podcasterplus.com/podcasts/b7e4c1a0-5f2d-4e8a-9c3b-1d6f8a2e4c90/images/cover.jpg"
			},
			"brand_color": "#7E22CE",
			"player_color": null,
			"links": {
				"listen": "https://listen.podcasterplus.com/acme-radio-hour"
			},
			"branding": {
				"show_powered_by": true
			}
		}
	}
}

Episode object

All episode list item fields, plus an embedded podcast summary:

FieldTypeNotes
podcast.idstring (UUID)
podcast.slugstring
podcast.titlestring
podcast.authorstring | null
podcast.artwork.urlstring | nullShow cover art
podcast.brand_colorstring | nullThe show's brand colour as a hex string
podcast.player_colorstring | nullThe player accent (#rrggbb); the player uses this first, then brand_color, then the default purple
podcast.links.listenstringShow listen page
podcast.branding.show_powered_bybooleanWhether embedded players display "Powered by PodcasterPlus"

An episode UUID only resolves while its parent show is publicly visible, so episode data can never leak past the show's visibility.

Errors: 400 invalid_request (not a UUID), 404 not_found.

GET /v1/oembed

oEmbed provider endpoint for listen-page URLs. Returns a rich-type response whose html is a player iframe.

Query parameters:

ParameterRequiredNotes
urlYesA listen-page URL (see the accepted shapes below)
formatNoOnly json is supported; anything else returns 501 not_implemented
maxwidthNoClamped to 280–1000. Default width is 600
maxheightNoValues below 300 select the compact player (height 110); otherwise height 300

Accepted url shapes (exactly these two, on listen.podcasterplus.com):

URLEmbeds
https://listen.podcasterplus.com/{podcastSlug}Latest-episode player
https://listen.podcasterplus.com/{podcastSlug}/e/{episodeSlug}That specific episode

Any other URL returns 404 not_found. The endpoint parses the URL; it never fetches it.

bash
curl "https://api.podcasterplus.com/v1/oembed?url=https%3A%2F%2Flisten.podcasterplus.com%2Facme-radio-hour%2Fe%2Fthe-big-launch&format=json"
json
{
	"version": "1.0",
	"type": "rich",
	"provider_name": "PodcasterPlus",
	"provider_url": "https://podcasterplus.com",
	"title": "The Big Launch",
	"author_name": "The Acme Radio Hour",
	"html": "<iframe src=\"https://embed.podcasterplus.com/ep/3f9d2b6e-8c41-4a57-b2f0-6e5d4c3b2a19?size=standard\" width=\"600\" height=\"300\" style=\"max-width:100%;border:0;border-radius:12px\" title=\"The Big Launch – audio player\" loading=\"lazy\" allow=\"autoplay\"></iframe>",
	"width": 600,
	"height": 300,
	"thumbnail_url": "https://media.podcasterplus.com/podcasts/b7e4c1a0-5f2d-4e8a-9c3b-1d6f8a2e4c90/images/cover.jpg",
	"thumbnail_width": 1400,
	"thumbnail_height": 1400,
	"cache_age": 3600,
	"documentation": "https://docs.podcasterplus.com/developers/"
}

The thumbnail_* fields are omitted when the show has no artwork. The oEmbed response follows the standard oEmbed shape rather than the API's {data} envelope.

Listen pages advertise this endpoint with an oEmbed discovery tag, so platforms that support oEmbed discovery (such as WordPress embed blocks) can turn a pasted listen URL into a player automatically:

html
<link
	rel="alternate"
	type="application/json+oembed"
	href="https://api.podcasterplus.com/v1/oembed?url=…&format=json"
/>

Errors: 400 invalid_request (missing url), 404 not_found (URL is not an embeddable listen page, or the target isn't publicly visible), 501 not_implemented (format other than json).


Understand what's visible

The API mirrors the visibility of the public RSS feed and listen pages, nothing more:

  • Shows must be hosted on PodcasterPlus and active (paused shows remain visible, matching their RSS feeds).
  • Episodes must be published, with a publish time in the past.
  • Descriptions are returned as plain text; owner contact details are never included.

If a show or episode isn't in its RSS feed, it isn't in the API.

Was this page helpful?