benecaster_cutoff_billing_pause_complete
What it is for: telling something outside Benecaster that a group of your subscribers just stopped being billed, so your own records do not go on saying they are paying customers. A CRM, an analytics warehouse, a Slack channel, your own emails — anything that mirrors subscriber state needs to hear about this, because nothing else will tell it.
Why the sweep happens at all: when a podcaster’s own Benecaster licence reaches Day-30 hard cutoff their feeds go dark, and Benecaster pauses collection on every built-in-membership Stripe subscription so nobody keeps paying full price for a feed that has stopped. This action reports the outcome of that sweep. It fires once per sweep, not once per subscriber.
Each subscription that paused successfully is tagged GRACE_TIMEOUT in benecaster_subscriptions.paused_reason (see PauseReasons for the full set), which is what lets the later resume restore exactly this cohort and nothing else.
Hook this for $failures and $skipped_shows, not for $paused. The count says the happy path worked; the two arrays say who is still being charged for a feed that has stopped. Benecaster already raises a dashboard notice naming the affected shows — hook this when a dashboard notice is not enough, which on a site with real subscribers it usually is not.
Pausing is not cancelling. A paused Stripe subscription keeps its history, payment method and customer relationship, and resumes cleanly if the licence is brought current. Nothing here cancels or refunds anything, and closing a subscription for good stays the podcaster’s decision.
The worked example covers both sweeps, including why a resume failure is the inverse harm and what to do about $skipped_shows.
React when a lapsed licence stops (and later restarts) subscriber billing
When a podcaster’s own Benecaster licence reaches Day-30 hard cutoff, Benecaster pauses collection on every built-in-membership Stripe subscription so nobody is charged for a feed that has stopped, and resumes exactly that cohort when the licence recovers. Both sweeps announce their outcome. Hook them if your add-on mirrors subscriber state anywhere else — a CRM, an analytics warehouse, a Slack channel, your own emails.
Four things about these hooks that are not obvious.
Pause fires at cutoff, not at grace start. During the 30-day grace window subscribers receive the full paid product, so they keep paying for it. Nothing billing-related fires anywhere in the grace lifecycle — if you need a signal earlier, hook benecaster_license_grace_started instead and treat it as a warning rather than a state change.
Resume touches only what Benecaster paused. A subscription the podcaster cancelled or paused themselves during the outage is never resumed, because the cohort is selected by a paused_reason tag Benecaster writes and by the row still being chargeable. Do not “help” by resuming the rest — you would restart billing the podcaster deliberately stopped.
$skipped_shows is not a rounding error, and 'test_mode' is the one to read carefully. A show in test mode cannot be reached through its live keyset, and nothing records which mode created a given subscription — so those subscribers are still being charged and Benecaster cannot stop it. If your add-on surfaces billing state anywhere, surface these too.
Only built-in membership is ever affected. Subscriptions in MemberPress, WooCommerce Subscriptions, PMPro, RCP or Patreon are not ours to pause and Benecaster never touches them. If your add-on bridges one of those, the podcaster stopping their own billing is a manual step and these hooks will not fire for it.
There are matching benecaster_cutoff_billing_pause_started / benecaster_cutoff_billing_resume_started actions carrying just the candidate count, fired before any Stripe call, so a sweep that dies mid-way is still bounded in your logs.
<?php
add_action( 'benecaster_cutoff_billing_pause_complete', 'my_addon_billing_paused', 10, 3 );
add_action( 'benecaster_cutoff_billing_resume_complete', 'my_addon_billing_resumed', 10, 3 );
/**
* @param int $paused Subscriptions successfully paused and tagged.
* @param array $failures [ [ 'subscription_id' => string, 'show_id' => int, 'message' => string ], … ]
* @param array $skipped_shows show_id => 'test_mode' | 'no_keys'
*/
function my_addon_billing_paused( int $paused, array $failures, array $skipped_shows ): void {
// The interesting argument is $failures, not $paused. A failure means
// that subscriber is STILL BEING CHARGED for a feed returning 403 —
// the one outcome worth waking somebody up for.
if ( [] === $failures && [] === $skipped_shows ) {
return;
}
my_addon_alert( sprintf(
'Benecaster paused %d subscriptions; %d failed, %d shows unreachable.',
$paused,
count( $failures ),
count( $skipped_shows )
) );
}
function my_addon_billing_resumed( int $resumed, array $failures, array $skipped_shows ): void {
// A resume failure is the inverse harm: those subscribers still have
// access and are NOT being billed for it.
my_addon_sync_billing_state();
}
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
$paused |
int |
— | Subscriptions successfully paused and tagged. |
$failures |
array |
— | **The argument that matters.** One entry per subscription the Stripe call failed on, each `{ subscription_id: string, show_id: int, message: string }`. Every entry is a subscriber still being charged for a feed that returns an error — the exact outcome this sweep exists to prevent. Empty on a clean run. Treat a non-empty array as an incident, not as telemetry. |
$skipped_shows |
array |
— | Shows the sweep could not act on at all, as `show_id => reason`. Two reasons: `test_mode` — the show is configured for test mode while holding live subscriptions, so Benecaster cannot reach them; `no_keys` — its Stripe credentials are missing or unusable. Every subscriber on a skipped show keeps paying. Empty on a clean run. |
Notes
Do not write to paused_reason from a callback on this hook. The column is the only local record that a subscription is paused at all — Stripe leaves subscription.status as active while pause_collection is set — and the resume sweep selects on it. See PauseReasons.
Need this built rather than just documented? See our services →