Skip to content

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 ​

  1. 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/podcasts lists your podcasts with their IDs if the key also has the Podcasts permission, and GET /v1/me lists 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.

    bash
    PODCAST_ID=b7e4c1a0-5f2d-4e8a-9c3b-1d6f8a2e4c90
  2. Ask for the last 30 days of downloads:

    bash
    curl "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 ​

  1. Open the episode workspace and choose More episode actions (⋮) → Copy episode ID, or request GET /v1/me/podcasts/{podcast_id}/analytics/episodes with your Analytics key.
  2. In the API response, find the title and use its episode_id for /v1/me/episodes/{episode_id}/analytics/downloads. No Episodes permission is needed; you must own the podcast.
  3. Follow pagination.next_cursor as the cursor query parameter if needed: the list defaults to 20 results, with limit=100 available. 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:

ParameterMeaning
preset7d, 30d, 90d, 12m or all, ending today. The default is 30d.
fromThe first day, YYYY-MM-DD.
toThe 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:

  • from and to are the window actually used;
  • clamped is true and clamp_reason is retention when your plan's window cut the start;
  • clamp_reason is collection_start when you asked for days before show.fm started counting downloads (1 July 2026). No plan has data before that;
  • retention_days is your plan's window, or null for unlimited history;
  • data_start is 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:

FieldMeaning
sourceWhere the downloads come from: show.fm, or your host (for example transistor) for a podcast hosted elsewhere.
verified_fromThe 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_throughThe last day of that run. Every day from verified_from to complete_through has been fully counted, with no gap.
revisable_daysHow 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_partialtrue when the range includes today, which is still being counted.
last_updated_atWhen 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, with unique_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.source and meta.methodology.source name 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_listeners and peak_daily_unique_listeners are null. 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 ​

Statuserror.codeWhen
400invalid_requestA bad parameter. error.details[0].field names it; reason is outside_retention or before_collection_start for a range with nothing to show.
401invalid_api_keyThe key is unknown, revoked or expired.
403insufficient_scopeThe key does not have the Analytics permission.
403forbiddenYou are not the owner of this podcast.
403plan_upgrade_requiredThe plan does not include the API, or the endpoint needs a feature it lacks (devices, platforms).
404not_foundThe podcast or episode does not exist, is not yours, or is not on the key. All three look the same.
429rate_limitedToo 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.

Was this page helpful?