Read the Analytics Dashboard downloads by tier, by podcast app and by episode
GET benecaster-analytics/v1/analytics/downloads reports tracked feed downloads for a date range: total, tiers ({ tier_slug, count }, largest first; untiered downloads are under public), apps ({ app, count }, largest first then by name) and the top episodes, each { episode_id, show_id, title, count, tiers }.
It takes show_id, date_from, date_to and an optional scope (show, the default, or all), with the same permission and errors as the other Analytics Dashboard routes (see Read or extend the Analytics Dashboard growth series and Read every Analytics Dashboard route for All my shows), plus an optional limit of episodes (default 5, at most 100). A tier_slug is accepted and ignored: Benecaster cannot rank episodes by one tier, so the answer’s tier_slug is always null. Every figure is read through benecaster_get_download_total() and benecaster_get_downloads_by(), so days older than the 30-day raw download log are included. On Benecaster’s admin page the add-on publishes its own REST base as window.benecasterAnalyticsDashboard.apiUrl, which works under every permalink setting:
const url = window.benecasterAnalyticsDashboard.apiUrl
+ 'analytics/downloads?show_id=12&date_from=2026-08-01&date_to=2026-10-28&limit=10';
const downloads = await ( await fetch( url, { headers: { 'X-WP-Nonce': window.benecasterAdmin.nonce } } ) ).json();
if ( downloads.proxy_required ) {
// No show in the request tracks downloads: explain how to switch it on.
}
// downloads.episodes[0] → { episode_id: 88, show_id: 12, title: 'Pilot', count: 412, tiers: [ { tier_slug: 'gold', count: 300 }, … ] }
// downloads.apps → [ { app: 'Apple Podcasts', count: 610 }, { app: 'Overcast', count: 240 }, { app: 'Other', count: 12 } ]
To change what every panel, export and digest sees, filter the downloads, as below.
- A show counts as tracking downloads when its Enable download tracking on all episodes setting is on, or any published episode is set to Force on for this episode. When no show in the request tracks them, the answer is empty with
proxy_required: true, never an error;showslists{ show_id, tracking }for each show read. A show that has stopped tracking is left out even if it has past downloads. - Downloads a show refused (a lapsed licence, the Day-30 cut-off) write no row, by design, so a lapsed show’s figures drop to followers only. Nothing in the answer can tell “nobody tried” from “everybody was refused”; do not build a churn signal on it.
- The figures count feed downloads only: the episode page’s download link serves the player’s URL and never goes through the tracking proxy.
- An episode’s
countis this show’s; itstiersare the episode’s whole history in the range, including downloads from before it moved from another show. appsisbenecaster_get_downloads_by( 'app', … )for each show that tracks downloads, summed by label. The labels are Benecaster’s, in English: named by thebenecaster_download_app_patternsfilter,Otherwhen no pattern matches the User-Agent andUnknownwhen there was none (the Analytics screen shows those two as Other apps and Unknown app, andBrowseras Web browser). The counts add up tototal. There is no app split per episode or per tier.- The filter’s
$argsareshow_ids,date_from,date_to,tier_slug(alwaysnull) andepisode_limit; a non-array return is ignored.
Code
<?php
// Leave a staff-only tier out of every panel, export and digest.
add_filter( 'benecaster_analytics_downloads', function ( array $data, array $args ): array {
$data['tiers'] = array_values( array_filter( $data['tiers'], fn( $t ) => 'staff' !== $t['tier_slug'] ) );
return $data;
}, 10, 2 );
Hooks Used
Need this built rather than just documented? See our services →