Skip to main content

Read the Analytics Dashboard downloads by tier, by podcast app and by episode

Premium Intermediate

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; shows lists { 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 count is this show’s; its tiers are the episode’s whole history in the range, including downloads from before it moved from another show.
  • apps is benecaster_get_downloads_by( 'app', … ) for each show that tracks downloads, summed by label. The labels are Benecaster’s, in English: named by the benecaster_download_app_patterns filter, Other when no pattern matches the User-Agent and Unknown when there was none (the Analytics screen shows those two as Other apps and Unknown app, and Browser as Web browser). The counts add up to total. There is no app split per episode or per tier.
  • The filter’s $args are show_ids, date_from, date_to, tier_slug (always null) and episode_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 );

View on GitHub →

Hooks Used

Need this built rather than just documented? See our services →