Skip to main content

Track the payment-failed license grace lifecycle

Premium Intermediate

A failed payment on the podcaster’s own Benecaster subscription opens a 30-day grace window. Three actions mark its start, its expiry, and its recovery — enough to drive an admin notice, an external alert, a customer email, or a segmentation column in a reporting pipeline.

These fire on the podcaster’s own WordPress install, not on benecaster.com. The subject is their license, but GraceExpiryTracker runs locally off the daily /validate response, so this is ordinary plugin code belonging in a site’s own add-on.

What actually happens at day 30: the private feeds go dark. Every active token is bulk-revoked with a grace_timeout reason and token requests return 403. Subscribers are not moved onto the public feed. That silent-downgrade behaviour existed once and was removed deliberately — it left subscribers believing they still had what they paid for while hiding from the podcaster the one symptom that would have prompted them to fix their card. Anything you write here should say “your feeds have stopped”, never “your subscribers are on the public feed”.

Only payment_failed opens a window. A cancelled or license_not_found response does not, so _grace_started never fires for those.

Which hook for which job.

Moment Hook Payload
Payment fails, window opens benecaster_license_grace_started ( int $started_at, string $reason )
Day 30, access cut off benecaster_license_payment_grace_hard_cutoff ( int $cutoff_at, int $token_count )
Billing fixed after a cutoff benecaster_license_payment_grace_recovered ( int $restored_count, int $started_at )

Prefer the two payment_grace hooks over their older twins. benecaster_license_grace_expired fires at the same instant as the hard-cutoff action and is retained only for listeners written before it existed; the newer hook additionally carries $token_count, which is the figure that makes an alert actionable. Likewise benecaster_license_grace_recovered fires on every recovery — including a day-5 pay-and-carry-on where nothing was ever revoked — whereas benecaster_license_payment_grace_recovered fires only when a cutoff was actually cleared, and hands you the restored count directly.

Do not guard these against repeat firing. Each fires at most once per grace cycle: the cutoff tick returns early on every later cron pass because the hard-cutoff marker is already set. A de-duplication flag will never see a second call, and the stale flag then has to be cleaned up on recovery for no benefit. Do not infer cutoff state from elapsed time either — a $duration_seconds >= 30 * DAY_IN_SECONDS test looks equivalent to “the cutoff fired” and is not, because the cutoff lands on the first cron tick after day 30. Hooking benecaster_license_payment_grace_recovered answers the question exactly.

Code

<?php
add_action( 'benecaster_license_grace_started', function ( int $started_at, string $reason ): void {
    // Payment just failed — the 30-day window is open and subscribers are
    // unaffected for now. Warn the operator while it is still cheap to fix.
    $body = sprintf(
        /* translators: %s: the /validate reason string. */
        __( 'Your Benecaster subscription failed a payment (%s). Your subscribers are unaffected for the next 30 days. If billing is not updated by then, their private feeds stop serving entirely.', 'my-addon' ),
        $reason
    );

    // Plain wp_mail() is deliberate: operator alert to the site's own admin address.
    // Use benecaster_mail() for anything a podcaster or subscriber reads.
    wp_mail( get_option( 'admin_email' ), __( 'Benecaster: payment failed', 'my-addon' ), $body );
}, 10, 2 );

add_action( 'benecaster_license_payment_grace_hard_cutoff', function ( int $cutoff_at, int $token_count ): void {
    // Day 30. $token_count tokens were just revoked and those feeds now
    // return 403 — subscribers are NOT on the public feed. Fires once per
    // grace cycle, so no de-duplication guard is needed.
    my_addon_alert( 'license_hard_cutoff', [
        'cutoff_at'     => $cutoff_at,
        'feeds_stopped' => $token_count,
    ] );
}, 10, 2 );

add_action( 'benecaster_license_payment_grace_recovered', function ( int $restored_count, int $started_at ): void {
    // Billing fixed after a cutoff. Fires ONLY when tokens were actually
    // revoked, so reaching this callback is itself the signal — no duration
    // arithmetic. Restored subscribers keep their original feed URLs, so
    // nobody has to re-add anything.
    my_addon_alert( 'license_recovered', [
        'feeds_restored' => $restored_count,
        'outage_seconds' => time() - $started_at,
    ] );
}, 10, 2 );

View on GitHub →

Hooks Used

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