Appearance
API guide
The show.fm API offers keyless access to published podcast content and authenticated access to your own podcasts, episodes and analytics. This guide explains conventions and common requests. Use the API reference for the documented parameters, responses and language samples for each endpoint.
Base URL: https://api.show.fmVersion prefix: /v1Methods: GET, HEAD, and OPTIONS only
Follow the request conventions
Response envelope
Most JSON responses use the envelopes below. oEmbed returns its own payload, and a 304 response has no body:
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: invalid episode UUID, bad cursor, invalid list filters, missing parameter. error.details names the field, sometimes with a reason |
| 401 | unauthorized | Keyed endpoints only: no Authorization: Bearer header |
| 401 | invalid_api_key | Keyed endpoints only: the key is malformed, unknown, revoked or expired |
| 403 | insufficient_scope | Keyed endpoints only: the key lacks the permission the endpoint needs |
| 403 | forbidden | Keyed endpoints only: role, team-key setting or Terms not accepted |
| 403 | plan_upgrade_required | Keyed endpoints only: the podcast's plan does not include the API, or the feature an endpoint needs (device breakdowns, Apple and Spotify imports) |
| 403 | unavailable | A suspended show whose requested content is otherwise public. The message is "This episode can't be played right now." |
| 404 | not_found | The resource doesn't exist or isn't publicly visible |
| 429 | rate_limited | Read limit reached; follow the response’s Retry-After header |
| 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 |
The keyless API returns 429 with the rate_limited error envelope when its fair-use limit is reached. These responses include Retry-After: 60, Cache-Control: no-store, and Access-Control-Allow-Origin: *. Wait at least 60 seconds, then retry with jitter.
Pagination
List endpoints use opaque cursor pagination. The following limits and ordering describe the public episode list; keyed endpoints have their own limits in the API reference:
limit: items per page. Default20, maximum50. Integer values outside the range are clamped; non-integer values returninvalid_request.season: include only a season number, an integer from0to9999.type: include only the comma-separated types you name:full,trailer,bonus. For example,type=fullexcludes trailers and bonus episodes.cursor: thepagination.next_cursorvalue from the previous page. Treat it as an opaque token; a tampered or malformed cursor returnsinvalid_request.
Keep the same season and type filters on every page. Changing, adding or removing filters with a cursor returns 400 invalid_request. Type order and duplicate types do not change the filter. Filtered pages bypass the API's payload cache but keep the normal HTTP cache headers.
When next_cursor is null, you've reached the last page.
bash
# First page
curl "https://api.show.fm/v1/podcasts/acme-radio-hour/episodes?limit=20"
# Next page
curl "https://api.show.fm/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 keyless content 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.show.fm/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 keyless API is callable directly from browsers on any origin:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, HEAD, OPTIONS
Access-Control-Max-Age: 86400Keyless requests do not use credentials. Keyed /v1/me endpoints have no CORS support; call them from your server.
Authentication
The public endpoints below are keyless, and an Authorization header sent to them is ignored. The keyed endpoints under /v1/me need an API key as a Bearer token and follow a stricter contract: no CORS, Cache-Control: private, no-store, and rate-limit headers on every response.
OpenAPI
The whole v1 surface, keyless and keyed, is described in an OpenAPI 3.1 document at https://api.show.fm/v1/openapi.json (also here). Generate a client from it, or import it into Postman or Insomnia.
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.
- Podcast references: a podcast UUID or slug on
/v1/podcasts/{slug}and all its episode routes. A UUID resolves to the current slug and returns the same payload. Store UUIDs when you need a stable reference. - Episode IDs: UUIDs.
Malformed podcast or episode references return 400 invalid_request. A well-formed but unknown slug or UUID returns 404 not_found. Invalid list parameters also return 400 invalid_request.
Endpoints
GET /v1
API index. Returns the API name, version, and a link to these docs.
bash
curl https://api.show.fm/v1json
{
"data": {
"name": "show.fm Public API",
"version": "v1",
"documentation": "https://show.fm/docs/developers/"
}
}GET / returns the same body.
GET /v1/podcasts/{slug}
Show metadata for one podcast. The {slug} path parameter accepts either its UUID or its slug.
Renamed shows redirect
A show's address (slug) can change. Requests using a previous address answer a permanent 301 to the canonical path: follow redirects and your stored URLs keep working forever. The links.feed URL in payloads uses the show's permanent identifier and never changes; prefer storing episode id values, which are also permanent.
bash
curl https://api.show.fm/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://m.cdn.media/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://acme-radio-hour.show.fm",
"feed": "https://rss.cdn.media/b3d1a9c2-4e5f-4a6b-8c7d-9e0f1a2b3c4d"
},
"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 show.fm" |
Errors: 400 invalid_request (malformed reference), 404 not_found (unknown, inactive or externally hosted show), 403 unavailable (otherwise-public suspended 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), season (integer 0 to 9999), type (comma-separated include list of full, trailer, bonus). See Pagination.
bash
curl "https://api.show.fm/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://m.cdn.media/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://m.cdn.media/podcasts/b7e4c1a0-5f2d-4e8a-9c3b-1d6f8a2e4c90/images/cover.jpg"
},
"links": {
"listen": "https://acme-radio-hour.show.fm/e/the-big-launch",
"embed": "https://embed.cdn.media/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://m.cdn.media/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://m.cdn.media/podcasts/b7e4c1a0-5f2d-4e8a-9c3b-1d6f8a2e4c90/e/8a1f3c5e-2b4d-4f6a-9c8e-7d5b3a1f9e2c/images/cover.jpg"
},
"links": {
"listen": "https://acme-radio-hour.show.fm/e/prototypes-and-regrets",
"embed": "https://embed.cdn.media/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 |
artwork.own | boolean (optional) | true for an episode cover, false for the show-cover fallback. Missing in older cached responses means unknown. |
links.listen | string | Public episode page: https://{slug}.show.fm/e/{episodeSlug} |
links.embed | string | Iframe player page: https://embed.cdn.media/ep/{id} |
Errors: 400 invalid_request (malformed reference, non-integer limit, bad cursor, invalid filters or changed cursor filters), 404 not_found (unknown show), 403 unavailable (otherwise-public suspended 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.show.fm/v1/podcasts/acme-radio-hour/episodes/latestErrors: 400 invalid_request (malformed reference), 404 not_found (unknown show, or no published episodes), 403 unavailable (otherwise-public episode of a suspended show).
GET /v1/podcasts/{slug}/episodes/{idOrSlug}
One episode of a show, addressed by either its UUID or its episode slug. Returns the full episode shape.
bash
curl https://api.show.fm/v1/podcasts/acme-radio-hour/episodes/the-big-launch
curl https://api.show.fm/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. An old podcast slug redirects a visible episode to the current slug. If that podcast is suspended, an otherwise-visible episode returns 403 unavailable after its membership is checked. Hidden and foreign episodes still return 404 not_found.
Errors: 400 invalid_request (malformed podcast or episode reference), 404 not_found (unknown or non-public episode), 403 unavailable (otherwise-public episode of a suspended show).
GET /v1/episodes/{id}
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.show.fm/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://m.cdn.media/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://m.cdn.media/podcasts/b7e4c1a0-5f2d-4e8a-9c3b-1d6f8a2e4c90/images/cover.jpg"
},
"links": {
"listen": "https://acme-radio-hour.show.fm/e/the-big-launch",
"embed": "https://embed.cdn.media/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://m.cdn.media/podcasts/b7e4c1a0-5f2d-4e8a-9c3b-1d6f8a2e4c90/images/cover.jpg"
},
"brand_color": "#7E22CE",
"player_color": null,
"links": {
"listen": "https://acme-radio-hour.show.fm"
},
"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 show.fm" |
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, 403 unavailable (otherwise-public episode of a suspended show).
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 | Never exceeded; capped at 1000. Default width is 600 |
maxheight | No | Never exceeded. Values below 300 select the compact player (height 110); otherwise height 300 |
The returned dimensions never exceed your maxwidth/maxheight, as the oEmbed spec requires. The player is designed for a minimum of 280 × 110 (compact); requesting maxima below that returns a smaller, clipped embed rather than an oversized one, so keep your maxima at or above the design minimum for best results.
Accepted url shapes (the two customer-page forms on {slug}.show.fm; the legacy listen.podcasterplus.com/{slug} path forms remain accepted):
| URL | Embeds |
|---|---|
https://{podcastSlug}.show.fm | Latest-episode player |
https://{podcastSlug}.show.fm/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.show.fm/v1/oembed?url=https%3A%2F%2Facme-radio-hour.show.fm%2Fe%2Fthe-big-launch&format=json"json
{
"version": "1.0",
"type": "rich",
"provider_name": "show.fm",
"provider_url": "https://show.fm",
"title": "The Big Launch",
"author_name": "The Acme Radio Hour",
"html": "<iframe src=\"https://embed.cdn.media/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://m.cdn.media/podcasts/b7e4c1a0-5f2d-4e8a-9c3b-1d6f8a2e4c90/images/cover.jpg",
"thumbnail_width": 1400,
"thumbnail_height": 1400,
"cache_age": 3600,
"documentation": "https://show.fm/docs/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.show.fm/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).
Your podcasts (keyed)
These endpoints read your own data with an API key. Call them from a server: keyed responses carry no CORS headers, so browsers are refused.
Podcast and episode IDs in a path are UUIDs and may be written in any letter case; responses always give them in lowercase.
Every keyed response, success or error, carries the caching and request-id headers below. Responses to a verified key also carry the rate-limit headers for the allowance that applied; a refusal before the key is verified (no key, a malformed key, a key in the URL) does not.
Cache-Control: private, no-store
Vary: Authorization
X-Request-Id: 5f0c…
RateLimit-Policy: "account";q=60;w=60
RateLimit: "account";r=59;t=42Quote X-Request-Id if you contact support about a request. Lists use cursor pagination like the lists above, but limit must be from 1 to 100 (default 20): a value outside that range gets 400 invalid_request rather than being adjusted.
GET /v1/me
The key you are using and the user it acts for. Needs only a valid key.
bash
curl https://api.show.fm/v1/me -H "Authorization: Bearer $SHOWFM_API_KEY"json
{
"data": {
"user": { "id": "0e190000-0000-4000-8000-000000000001", "name": "Jess Doe" },
"key": {
"id": "1e275baf-43f5-4955-96ef-087cefaa1028",
"name": "Looker dashboard",
"environment": "live",
"scopes": ["podcasts:read"],
"podcasts": ["b7e4c1a0-5f2d-4e8a-9c3b-1d6f8a2e4c90"],
"expires_at": "2027-10-01T09:00:00.000Z",
"previous_secret_in_use": false,
"previous_secret_expires_at": null
}
}
}podcasts is "all" for a key that covers every podcast you can access. previous_secret_in_use is true while you are still sending the old secret of a key you rolled with an overlap: switch to the new one before previous_secret_expires_at.
Keyed paths name podcasts and episodes by ID, never by web address. Find your podcast and episode IDs shows where to get them.
GET /v1/me/podcasts
Scope: podcasts:read. The podcasts this key can read now, oldest first: podcasts on the key that you are a current member of, whose plan includes the API, and (unless you own them) whose owner allows team members' keys.
bash
curl "https://api.show.fm/v1/me/podcasts?limit=20" -H "Authorization: Bearer $SHOWFM_API_KEY"json
{
"data": [
{
"id": "b7e4c1a0-5f2d-4e8a-9c3b-1d6f8a2e4c90",
"slug": "acme-radio-hour",
"title": "The Acme Radio Hour",
"role": "owner",
"status": "active",
"hosting_type": "podcasterplus",
"timezone": "Europe/London",
"created_at": "2026-01-14T10:00:00.000Z"
}
],
"pagination": { "next_cursor": null }
}GET /v1/me/podcasts/{podcast_id}
Scope: podcasts:read. One of your podcasts by id, in the same shape as a list item.
Errors: 404 not_found for any podcast the key cannot reach, whether or not it exists; 403 forbidden if the owner turned off team members' keys; 403 plan_upgrade_required if its plan does not include the API.
Analytics
Scope: analytics:read, on podcasts you own. Every response except platforms carries meta.range, meta.freshness and meta.methodology. Full guide: Analytics API guide.
| Endpoint | Returns |
|---|---|
GET /v1/me/podcasts/{podcast_id}/analytics/downloads | Downloads per day, week or month, with unique listeners per day, and totals |
GET /v1/me/podcasts/{podcast_id}/analytics/episodes | Downloads per episode in the range, all time and in the first week |
GET /v1/me/podcasts/{podcast_id}/analytics/breakdowns?dimension=… | Downloads by apps, countries, devices, sources or referrers |
GET /v1/me/podcasts/{podcast_id}/analytics/platforms?source=… | Your latest Apple or Spotify import |
GET /v1/me/episodes/{episode_id}/analytics/downloads | One episode's series and lifetime totals |
Your episodes (keyed)
Match imported episodes
Both the keyed episode list and episode detail return source fingerprints with the existing episodes:read scope:
source.enclosure_sha256is the lowercase SHA-256 of the normalised source-feed audio address. It comes from the earliest completed import snapshot or stored external metadata. It is never guessed from current audio or an original upload.source.guid_sha256is the lowercase SHA-256 of the original RSS import or external-feed GUID after trimming surrounding whitespace only. It remains available for authorised reads of unpublished episodes, and is null when the source GUID carries a credential.rss_guidis the GUID in the show's public RSS feed, which for imported episodes is usually the original GUID. It is null unless the episode is published and its publication time has passed; existing credential restrictions still apply.
The source object never returns a raw URL or GUID. Unknown values and native episodes without import or external metadata have null source fingerprints. The enclosure fingerprint is also null for invalid URLs and until the episode is public. Follow the exact normalisation rules and 64 portable parity vectors before hashing values from an old embed or feed.
For embed migration, request status=published, follow every pagination.next_cursor, and match the enclosure fingerprint first, then the source GUID fingerprint or plain rss_guid, then title and publication date. Keep ambiguous matches for manual review. These fields retain the existing podcast, role and episode access checks and private, no-store caching rules.
Scopes: episodes:read for episodes, people and credits; transcripts:read for transcripts. Owners and admins see every episode of the podcast; a Co-host sees only the episodes they are on, and any other episode answers 404 exactly as if it did not exist. Episodes are listed most recently created first, people oldest first, credits in their display order and transcript segments in spoken order, up to 100 a page, with pagination.next_cursor.
| Endpoint | Returns |
|---|---|
GET /v1/me/podcasts/{podcast_id}/episodes?status=&cursor=&limit= | Episodes of all statuses (draft, scheduled, published, archived, pending_confirmation), lean fields, import provenance and RSS GUID |
GET /v1/me/episodes/{episode_id} | One episode with description and show_notes as plain text, show_notes_html, and media |
GET /v1/me/episodes/{episode_id}/people | The roster: name, role_label, is_active, source_type. Never an email address or phone number |
GET /v1/me/episodes/{episode_id}/credits | The credits the RSS feed publishes, in their display order: name, role_label, external_url, display_order, and bio and avatar_url once the episode is public |
GET /v1/me/episodes/{episode_id}/transcript?cursor=&limit= | The transcript: episode_id, language, word_count, duration_seconds, total_segments, and a page of segments, each with id (a number), start, end, speaker, text |
Set $EPISODE_ID by opening the episode workspace and choosing More episode actions (⋮) → Copy episode ID, or take data[].id from the Episodes-permission list above. An Analytics-only key can instead read data[].episode_id from /v1/me/podcasts/{podcast_id}/analytics/episodes, on a podcast you own. That list includes episodes whose publication date has passed and other episodes with downloads, so it may omit drafts and scheduled episodes.
Both lists default to 20 results and allow limit=100. Pass pagination.next_cursor as cursor to the same endpoint for each further page, until it is null. See Find your podcast and episode IDs for full discovery and pagination examples. Use the UUID, never a slug or provider ID; finding an ID does not grant any additional permissions.
bash
curl "https://api.show.fm/v1/me/episodes/$EPISODE_ID" -H "Authorization: Bearer $SHOWFM_API_KEY"json
{
"data": {
"id": "4a1d0c2e-7b1f-4c55-9a0e-2f3b9d8c1a77",
"podcast_id": "b7e4c1a0-5f2d-4e8a-9c3b-1d6f8a2e4c90",
"slug": "the-finale",
"source": { "guid_sha256": null, "enclosure_sha256": null },
"rss_guid": "4a1d0c2e-7b1f-4c55-9a0e-2f3b9d8c1a77",
"title": "Episode 42: The finale",
"status": "published",
"episode_number": 42,
"season_number": 2,
"episode_type": "full",
"published_at": "2026-09-24T07:00:00.000Z",
"scheduled_for": null,
"duration_seconds": 2712,
"subtitle": "We say goodbye",
"created_at": "2026-09-10T15:22:01.000Z",
"explicit": false,
"description": "The last episode of the season.",
"show_notes": "Thanks to everyone who listened. Links: our site",
"show_notes_html": "<p>Thanks to everyone who listened.</p><p>Links: <a rel=\"nofollow noopener noreferrer\" href=\"https://example.com/\">our site</a></p>",
"media": {
"audio": {
"url": "https://m.cdn.media/…/audio.mp3",
"content_type": "audio/mpeg",
"size_bytes": 43401234,
"duration_seconds": 2712
},
"cover_image_url": "https://m.cdn.media/…/cover.jpg",
"chapters_url": null,
"transcript_url": "https://m.cdn.media/…/transcript.vtt"
}
}
}Before an episode is public, nothing about its media leaves the API. Until it is published and its publish time has passed, media is null, a credit's bio and avatar_url are null, and a link to the episode's media pasted into its show notes, description or subtitle is replaced with [link removed], as is any link carrying an access token or code (a recording-studio join link, for example). If the notes hide such a link in a way the HTML cannot be cleaned of, show_notes_html is null and show_notes carries the notes. Once public, the URLs are the same ones your RSS feed publishes.
Show notes HTML is sanitised but still untrusted. show_notes_html keeps only paragraphs, line breaks, bold, italic, underline, links, lists, headings and quotes; links keep only an https, http or mailto address and gain rel="nofollow noopener noreferrer". Sanitise it again with your own library before you render it. show_notes (plain text) is always there too.
Transcripts are other people's speech. Treat the text as untrusted input. Links carrying an access token are replaced with [link removed].
Understand what's visible
This section is about the keyless public endpoints. The keyed endpoints under /v1/me read your own podcasts, including drafts and scheduled episodes, under the rules in Your podcasts (keyed) and Your episodes (keyed).
The keyless API mirrors the visibility of the public RSS feed and listen pages, nothing more:
- Shows must be hosted on show.fm 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 keyless API.
A show suspended for overage returns 403 unavailable on podcast and episode-list requests. An episode request returns this status only if the episode is otherwise public. Scheduled, unpublished, archived, deleted, blocked and unknown episodes keep the same 404 not_found response, including on a suspended show. The unavailable message is "This episode can't be played right now." These 403 responses use Cache-Control: no-store.