Skip to main content

Which subscriber count is the paying one

Free Intermediate

Benecaster issues a token to three different populations: paying subscribers, free-tier bridge members (a [Free] MemberPress level mapped into Benecaster, say), and direct followers who came in through [benecaster_follower_signup] with no bridge at all.

Counting “paying subscribers” means excluding both non-paying populations — and they are excluded by different rules:

  • Token type excludes only the types core exempts from paying
    (TokenRepository::PAYING_EXEMPT_TOKEN_TYPES: follower and purchaser; direct followers have no tier mapping at all). This is a deny list, not an allow list of subscriber: any other type counts as paying, including one an add-on defines that core has never heard of. The list can grow, and already has.
  • A free tier excludes members sitting on it. Those rows are subscriber-type. “Free” has to be
    asked of two tables: benecaster_tier_map for a show running an external bridge, and benecaster_membership_tiers for a show on built-in membership, which has no tier-map rows at all. A query that inner-joins only the tier map counts zero paying subscribers on a built-in-membership show, however many there are. A tier neither table knows about counts as paying, so the count can never fall short.

Applying only one rule miscounts, and does so silently. Check only the mapping and followers slip through; check only the token type and free-tier bridge members do. Both, or the number is wrong in a way nothing will tell you about. ⚠ Filtering on token_type = 'subscriber' instead of the exempt-type deny list under-counts: any non-standard type on a paid tier (a custom type your own code writes, or an add-on’s) is real paying capacity that a = 'subscriber' check silently drops.

Use the functions

$count      = benecaster_count_paying_subscribers( $show_id );
$site_count = benecaster_count_paying_subscribers_site_wide();

Both apply exactly the two rules above, so they cannot drift from Benecaster’s own definition of “paying” the way a copied query can.

Do not reach for benecaster_get_subscriber_count() here. It counts every active token for the show — followers and free-tier members included — which is precisely the miscount this recipe exists to prevent. It is the right function for audience size and the wrong one for anything about money. See Looking Up a Subscriber for the two side by side.

The site-wide count counts tokens, not people. A subscriber paying for two shows counts twice, so it equals the sum of the per-show counts. That is what the licence server counts and what is enforced against your plan’s subscriber limit. Core exposes no distinct-people count.

Neither function is filterable, deliberately. These are the numbers the plan threshold is decided from, and a filter would invite integrations to “adjust” a figure whose only useful property is that it says what is actually in the database.

Which of the three to use

  1. The public functions — the default, and the right answer almost every time. They need nothing
    but WordPress and Benecaster loaded: a theme template, a snippet plugin, a WP-CLI command in a sibling plugin, another add-on. No container, no class import.
  2. TokenRepository::count_paying_by_show() / count_paying_site_wide() — when you already hold
    the repository, typically inside an add-on that resolved it from the container for something else. Saves constructing a second instance; otherwise identical.
  3. Hand-rolled SQL — only when WordPress is not loaded at all: a query typed into a SQL client, a
    reporting tool pointed at the database, a migration script that never boots WP.

If PHP can call benecaster_count_paying_subscribers(), writing the query yourself is a liability rather than a preference. A copied query is a snapshot of the schema on the day you copied it, and it will keep returning a plausible-looking number long after it stops being the right one. ⚠ Hand-rolled SQL is not needed outside the container — the function works from a WP-CLI command in another plugin just as well as it does inside one.

The query below is shown for the third case, and because seeing it is how the two-rule requirement stops being something you have to take on trust.

Code

<?php
// Almost always: the public helpers. Both exclusions applied for you.
$count      = benecaster_count_paying_subscribers( $show_id );
$site_count = benecaster_count_paying_subscribers_site_wide();

// For contrast — every active token, followers and free-tier members
// included. An audience size, not a paying count.
$audience = benecaster_get_subscriber_count( $show_id );

// Only when WordPress is not loaded at all: a SQL client, a reporting
// tool, a migration script that never boots WP. Note BOTH conditions,
// and BOTH tier tables.
global $wpdb;

$tokens     = $wpdb->prefix . 'benecaster_tokens';
$tier_map   = $wpdb->prefix . 'benecaster_tier_map';          // external bridges
$mem_tiers  = $wpdb->prefix . 'benecaster_membership_tiers';  // built-in membership

// The exempt types, read from core so the query can't fall behind it.
$exempt       = \Benecaster\Token\TokenRepository::PAYING_EXEMPT_TOKEN_TYPES;
$placeholders = implode( ', ', array_fill( 0, count( $exempt ), '%s' ) );

// LEFT JOIN BOTH tier tables. A show uses one or the other, so an INNER JOIN
// to either one silently drops every token belonging to the other kind.
$joins = "LEFT JOIN {$tier_map} tm
              ON tm.show_id = t.show_id AND tm.internal_tier_slug = t.tier_slug
          LEFT JOIN {$mem_tiers} mt
              ON mt.show_id = t.show_id AND mt.tier_slug = t.tier_slug";

// "Paying" = not an exempt type, and not free according to whichever tier
// table holds a row. A tier NEITHER table knows about counts as paying:
// never under-count, because that is what the podcaster is billed on.
$paying = "t.token_type NOT IN ( {$placeholders} )
           AND ( CASE
                   WHEN tm.id IS NULL AND mt.id IS NULL THEN 0
                   ELSE LEAST( COALESCE(tm.is_free_tier, 1), COALESCE(mt.is_free, 1) )
                 END ) = 0";

// Count paying subscribers for a single show.
$count = (int) $wpdb->get_var( $wpdb->prepare(
    "SELECT COUNT(*) FROM {$tokens} t {$joins}
     WHERE t.show_id = %d AND t.status = 'active' AND {$paying}",
    $show_id,
    ...$exempt
) );

// Same pattern, site-wide — drives the daily license-server subscriber_count.
$site_count = (int) $wpdb->get_var( $wpdb->prepare(
    "SELECT COUNT(*) FROM {$tokens} t {$joins}
     WHERE t.status = 'active' AND {$paying}",
    ...$exempt
) );

// Outside PHP (a SQL client, a reporting tool): write the deny list
// literally, keep both LEFT JOINs, and re-check it whenever Benecaster
// updates: t.token_type NOT IN ('follower', 'purchaser')

View on GitHub →

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