Appearance
Analytics API guide
The analytics endpoints give your own code the same numbers as Analytics in the app: downloads over time, per episode, and by app, country, device, source or referrer, plus your Apple Podcasts and Spotify imports. Use them to feed a dashboard, a spreadsheet or a report for a sponsor.
They need an API key with the Analytics permission (analytics:read), and they work only on podcasts you own. Analytics are owner-only in the app, and the API follows the same rule: a key held by a team member gets 403 forbidden on these endpoints.
All analytics endpoints are read-only and live under https://api.show.fm/v1/me.
Quick start
Find your podcast's ID. The simplest way is to copy it from Settings → API keys in the app. From the API,
GET /v1/me/podcastslists your podcasts with their IDs if the key also has the Podcasts permission, andGET /v1/melists the IDs a key includes when you chose specific podcasts for it (a key for all podcasts shows"all"there instead). See Find your podcast and episode IDs.bashPODCAST_ID=b7e4c1a0-5f2d-4e8a-9c3b-1d6f8a2e4c90Ask for the last 30 days of downloads:
bashcurl "https://api.show.fm/v1/me/podcasts/$PODCAST_ID/analytics/downloads?preset=30d" \ -H "Authorization: Bearer $SHOWFM_API_KEY"
The answer:
json
{
"data": {
"podcast_id": "b7e4c1a0-5f2d-4e8a-9c3b-1d6f8a2e4c90",
"interval": "day",
"periods": [
{
"period_start": "2026-09-01",
"period_end": "2026-09-01",
"downloads": 412,
"unique_listeners": 389,
"partial": false
}
],
"totals": { "downloads": 11873, "peak_daily_unique_listeners": 702 }
},
"meta": {
"range": {
"requested_from": "2026-09-01",
"requested_to": "2026-10-01",
"from": "2026-09-01",
"to": "2026-10-01",
"clamped": false,
"clamp_reason": null,
"retention_days": 365,
"data_start": "2026-07-01"
},
"freshness": {
"source": "show.fm",
"verified_from": "2026-07-08",
"complete_through": "2026-09-29",
"revisable_days": 1,
"today_partial": true,
"last_updated_at": "2026-10-01T09:05:12.000Z"
},
"methodology": {
"source": "show.fm",
"guideline": "IAB Podcast Measurement Technical Guidelines v2.2 (aligned, not certified)",
"certified": false,
"download_definition": "One download per hashed IP address and user agent, per episode, per UTC day, after bot filtering and a minimum of 60 seconds of audio.",
"unique_listener_definition": "Distinct hashed IP address and user agent pairs in one UTC day. Never summed across days.",
"day_boundary": "UTC",
"docs_url": "https://show.fm/docs/analytics/"
}
}
}Find an episode for an episode-specific request
- Open the episode workspace and choose More episode actions (⋮) → Copy episode ID, or request
GET /v1/me/podcasts/{podcast_id}/analytics/episodeswith your Analytics key. - In the API response, find the title and use its
episode_idfor/v1/me/episodes/{episode_id}/analytics/downloads. No Episodes permission is needed; you must own the podcast. - Follow
pagination.next_cursoras thecursorquery parameter if needed: the list defaults to 20 results, withlimit=100available. It includes episodes whose publication date has passed and other episodes with downloads, so it may omit drafts and scheduled episodes.
See Find your podcast and episode IDs for copy failure recovery, pagination examples and the alternative Episodes-permission route, which returns id instead of episode_id. Slugs from workspace or public URLs do not work as episode IDs.
Choose a date range
Every analytics endpoint except platforms takes the same range parameters:
| Parameter | Meaning |
|---|---|
preset | 7d, 30d, 90d, 12m or all, ending today. The default is 30d. |
from | The first day, YYYY-MM-DD. |
to | The last day, YYYY-MM-DD. Needs from. Defaults to today. |
Use either preset or from (with an optional to), never both. Days are UTC days, and "today" is the current UTC date. A date after today is refused: the API never pads a range with future days that would read as real zeros.
Your plan's history
Each plan keeps a window of analytics history. If you ask for more than your plan includes, the API uses the part it can show and says so in meta.range:
fromandtoare the window actually used;clampedistrueandclamp_reasonisretentionwhen your plan's window cut the start;clamp_reasoniscollection_startwhen you asked for days before show.fm started counting downloads (1 July 2026). No plan has data before that;retention_daysis your plan's window, ornullfor unlimited history;data_startis the first day with any recorded download.
A range that ends before your plan's window starts has nothing to show, so it is refused with 400 invalid_request and error.details[0].reason set to outside_retention. A range that ends before 1 July 2026 gets before_collection_start.
Know how fresh the numbers are
meta.freshness tells you which figures are final:
| Field | Meaning |
|---|---|
source | Where the downloads come from: show.fm, or your host (for example transistor) for a podcast hosted elsewhere. |
verified_from | The first day of the run show.fm has counted with no gap. Days before it were counted at the time, but show.fm can no longer check that none was missed. For a connected host, it is the first day of the first pull. |
complete_through | The last day of that run. Every day from verified_from to complete_through has been fully counted, with no gap. |
revisable_days | How many of the latest complete days a later count may still adjust: 1 for show.fm (yesterday is counted again for a while as late requests arrive), 14 for a connected host (it restates its last two weeks every day), 0 for a podcast hosted elsewhere with no connection. |
today_partial | true when the range includes today, which is still being counted. |
last_updated_at | When the figures were last updated. |
Each period in a series carries partial: true when any of its days falls outside verified_from to complete_through, and every period is partial when either is null (nothing verified yet). If you store the numbers, overwrite partial periods on your next fetch, and refresh the last revisable_days days too.
After an outage, show.fm counts the missed days again, oldest first, and complete_through catches up. A gap too old to recount ends the run: verified_from moves past it, and the days before it stay partial.
Periods before verified_from stay partial: true for good, but their figures will not change, so you do not need to keep fetching them.
There is no point polling more often than last_updated_at changes: show.fm counts downloads once an hour. Every response has an ETag, so send If-None-Match and you get a 304 with no body when nothing changed.
Endpoints
GET /v1/me/podcasts/{podcast_id}/analytics/downloads
Downloads over time for the whole podcast. Add interval:
day(the default): one period per day, withunique_listeners;week: ISO weeks, Monday to Sunday;month: calendar months.
Weeks and months are UTC and clipped to your range, so the first and last periods can be shorter; period_start and period_end say exactly which days each covers. Unique listeners are counted per day and never added up, because the same listener on two days would count twice. Weeks and months give peak_daily_unique_listeners, the highest single day, instead. A daily series is limited to 1,100 days; use week or month for longer ranges.
GET /v1/me/episodes/{episode_id}/analytics/downloads
The same series for one episode, plus all_time: its total downloads, its downloads in the first seven days after its first download, and that first day.
GET /v1/me/podcasts/{podcast_id}/analytics/episodes
Downloads per episode, newest first, 20 to a page by default (limit up to 100, then follow pagination.next_cursor):
json
{
"data": [
{
"episode_id": "4a1d…",
"title": "Episode 42: The finale",
"published_at": "2026-09-24T07:00:00.000Z",
"downloads": 2310,
"downloads_all_time": 2310,
"first_week_downloads": 1984,
"first_day": "2026-09-24"
}
],
"pagination": { "next_cursor": "MjAyNi0wOS0xN1QwNzowMDowMC4wMDBafDE…" },
"meta": { "range": {}, "freshness": {}, "methodology": {} }
}downloads counts the chosen range; downloads_all_time and first_week_downloads are lifetime figures, as in the app. The list holds every episode whose publish date (published_at) is on or before now, and any other episode that has downloads. Scheduled episodes appear once their publish date passes.
GET /v1/me/podcasts/{podcast_id}/analytics/breakdowns
Downloads ranked by one dimension: apps, countries, devices, sources (the embedded player, the listen page, or direct) or referrers (the site an embedded player was on). Add episode_id to break down one episode of this podcast.
json
{
"data": {
"dimension": "apps",
"episode_id": null,
"available": true,
"reason": null,
"depth": 5,
"items": [
{ "key": "Apple Podcasts", "downloads": 5120 },
{ "key": "Spotify", "downloads": 3302 },
{ "key": "other", "downloads": 410 }
]
},
"meta": { "range": {}, "freshness": {}, "methodology": {} }
}You get the same depth as the app: the top 5, or the top 15 on a plan with advanced analytics, with everything else added into other, so the items add up to the total. The exception is referrers, which counts only downloads that came with a referring site. devices needs a plan with advanced analytics. For a podcast hosted elsewhere, the host does not share these breakdowns, so available is false, reason is host_does_not_share and items is empty.
GET /v1/me/podcasts/{podcast_id}/analytics/platforms
The latest Apple Podcasts or Spotify export you imported in the Engagement section of your Analytics workspace (how to import). Pass source=apple or source=spotify. Needs a plan with engagement imports.
json
{
"data": {
"podcast_id": "b7e4c1a0-5f2d-4e8a-9c3b-1d6f8a2e4c90",
"source": "apple",
"imported_at": "2026-09-30T08:12:00.000Z",
"as_of": "2026-09-30",
"show_metrics": { "followers": 1204 },
"episodes": [
{
"episode_id": "4a1d…",
"title": "Episode 42: The finale",
"metrics": { "plays": 880, "listeners": 610 }
}
]
},
"pagination": { "next_cursor": null }
}Each import replaces the previous one for that source, so there is no date range: you always get the latest import. Before your first import, imported_at is null and the lists are empty. Episodes are paged like other lists.
Podcasts hosted elsewhere
If you connected a podcast hosted with Transistor, show.fm reads its downloads from the host once a day:
meta.freshness.sourceandmeta.methodology.sourcename the host;- the host counts the downloads by its own rules, and its days follow its own time zone;
- the host does not share unique listeners, so
unique_listenersandpeak_daily_unique_listenersarenull. In the app's charts the listener line for these podcasts simply repeats the downloads; the API leaves it out rather than repeat it; - breakdowns are not available (see above);
- the last 14 days can still change as the host restates them (
revisable_days: 14).
A podcast hosted elsewhere without a connection has no download source: source is external, verified_from and complete_through are null, and every period is partial.
Errors
| Status | error.code | When |
|---|---|---|
| 400 | invalid_request | A bad parameter. error.details[0].field names it; reason is outside_retention or before_collection_start for a range with nothing to show. |
| 401 | invalid_api_key | The key is unknown, revoked or expired. |
| 403 | insufficient_scope | The key does not have the Analytics permission. |
| 403 | forbidden | You are not the owner of this podcast. |
| 403 | plan_upgrade_required | The plan does not include the API, or the endpoint needs a feature it lacks (devices, platforms). |
| 404 | not_found | The podcast or episode does not exist, is not yours, or is not on the key. All three look the same. |
| 429 | rate_limited | Too many requests. Wait Retry-After seconds. |
Most parameters are checked before anything else, so a malformed one gets 400 whatever podcast it names. The checks that depend on the podcast come after access: outside_retention, before_collection_start, the 1,100-day daily limit, and whether a breakdown's episode_id belongs to the podcast (a 404). A podcast you cannot reach therefore answers 404, whatever is wrong with the checks that depend on the podcast.
How downloads are counted
show.fm counts downloads in line with the IAB Podcast Measurement Technical Guidelines v2.2. We follow the guidelines but are not certified by the IAB. See Understanding analytics for the full method, and meta.methodology on every response for a short summary.