Appearance
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 status | error.code | When |
|---|---|---|
| 400 | invalid_request | Malformed input: bad slug, invalid UUID, bad cursor, missing parameter |
| 404 | not_found | The resource doesn't exist or isn't publicly visible |
| 500 | internal_error | Something went wrong on our side |
| 501 | not_implemented | A requested variant isn't supported (e.g. oEmbed format=xml) |
| 503 | service_unavailable | The 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. Default20, maximum50. Integer values outside the range are clamped; non-integer values returninvalid_request.cursor: thepagination.next_cursorvalue from the previous page. Treat it as an opaque token; a tampered or malformed cursor returnsinvalid_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 304Please 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: 86400No 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/v1json
{
"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-hourjson
{
"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
| Field | Type | Notes |
|---|---|---|
id | string (UUID) | Stable podcast identifier |
slug | string | URL slug; podcasters can change this, so prefer id for stable references |
guid | string | null | The show's permanent podcast:guid (Podcast 2.0) |
title | string | |
subtitle | string | null | |
author | string | null | |
description | string | null | Plain text; HTML is stripped server-side |
language | string | null | e.g. "en" |
category / subcategory | string | null | Apple Podcasts category taxonomy |
explicit | boolean | |
artwork.url | string | null | Show cover art |
brand_color | string | null | The show's brand colour as a hex string |
player_color | string | null | The show's player accent colour (#rrggbb); null = the default purple |
website_url | string | null | |
episode_count | number | Count of publicly visible (published) episodes |
links.listen | string | Public listen page |
links.feed | string | Public RSS feed |
branding.show_powered_by | boolean | Whether 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
| Field | Type | Notes |
|---|---|---|
id | string (UUID) | Stable episode identifier, safe to reference from embeds |
slug | string | Episode URL slug (can change; id never does) |
title | string | |
description | string | null | Plain text; HTML is stripped server-side |
season_number | number | null | |
episode_number | number | null | |
episode_type | string | null | "full", "trailer", or "bonus" |
explicit | boolean | |
published_at | string | ISO 8601 UTC |
audio.url | string | null | Direct audio URL, playable cross-origin in a plain <audio> element |
audio.content_type | string | null | e.g. "audio/mpeg" |
audio.duration_seconds | number | null | |
audio.size_bytes | number | null | |
artwork.url | string | null | Episode cover art, falling back to the show's cover art |
links.listen | string | Public episode page: https://listen.podcasterplus.com/{slug}/e/{episodeSlug} |
links.embed | string | Iframe 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/latestErrors: 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-6e5d4c3b2a19When 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-6e5d4c3b2a19json
{
"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:
| Field | Type | Notes |
|---|---|---|
podcast.id | string (UUID) | |
podcast.slug | string | |
podcast.title | string | |
podcast.author | string | null | |
podcast.artwork.url | string | null | Show cover art |
podcast.brand_color | string | null | The show's brand colour as a hex string |
podcast.player_color | string | null | The player accent (#rrggbb); the player uses this first, then brand_color, then the default purple |
podcast.links.listen | string | Show listen page |
podcast.branding.show_powered_by | boolean | Whether 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:
| Parameter | Required | Notes |
|---|---|---|
url | Yes | A listen-page URL (see the accepted shapes below) |
format | No | Only json is supported; anything else returns 501 not_implemented |
maxwidth | No | Clamped to 280–1000. Default width is 600 |
maxheight | No | Values below 300 select the compact player (height 110); otherwise height 300 |
Accepted url shapes (exactly these two, on listen.podcasterplus.com):
| URL | Embeds |
|---|---|
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.